Add eBay credentials, edit listings feature, fix category service token
This commit is contained in:
@@ -14,6 +14,9 @@ public class EbayCategoryService
|
||||
{
|
||||
private readonly EbayAuthService _auth;
|
||||
|
||||
// Static client — avoids socket exhaustion from per-call `new HttpClient()`
|
||||
private static readonly HttpClient _http = new();
|
||||
|
||||
public EbayCategoryService(EbayAuthService auth)
|
||||
{
|
||||
_auth = auth;
|
||||
@@ -26,15 +29,18 @@ public class EbayCategoryService
|
||||
|
||||
try
|
||||
{
|
||||
var token = await _auth.GetValidAccessTokenAsync();
|
||||
using var http = new HttpClient();
|
||||
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
http.DefaultRequestHeaders.Add("X-EBAY-C-MARKETPLACE-ID", "EBAY_GB");
|
||||
// Taxonomy API supports app-level tokens — no user login required
|
||||
var token = await _auth.GetAppTokenAsync();
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get,
|
||||
$"{_auth.BaseUrl}/commerce/taxonomy/v1/category_tree/3/get_category_suggestions" +
|
||||
$"?q={Uri.EscapeDataString(query)}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
request.Headers.Add("X-EBAY-C-MARKETPLACE-ID", "EBAY_GB");
|
||||
|
||||
var url = $"{_auth.BaseUrl}/commerce/taxonomy/v1/category_tree/3/get_category_suggestions" +
|
||||
$"?q={Uri.EscapeDataString(query)}";
|
||||
var response = await _http.SendAsync(request);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var json = await http.GetStringAsync(url);
|
||||
if (!response.IsSuccessStatusCode) return new List<CategorySuggestion>();
|
||||
var obj = JObject.Parse(json);
|
||||
|
||||
var results = new List<CategorySuggestion>();
|
||||
|
||||
@@ -91,6 +91,56 @@ public class SavedListingsService
|
||||
catch { /* ignore — user may have already deleted it */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing listing's metadata and regenerates its text export file.
|
||||
/// The listing must be the same object reference held in Listings.
|
||||
/// </summary>
|
||||
public void Update(SavedListing listing)
|
||||
{
|
||||
if (Directory.Exists(listing.ExportFolder))
|
||||
{
|
||||
// Replace the text export — remove old .txt files first
|
||||
foreach (var old in Directory.GetFiles(listing.ExportFolder, "*.txt"))
|
||||
{
|
||||
try { File.Delete(old); } catch { }
|
||||
}
|
||||
var safeName = MakeSafeFilename(listing.Title);
|
||||
var textFile = Path.Combine(listing.ExportFolder, $"{safeName}.txt");
|
||||
File.WriteAllText(textFile, BuildTextExport(
|
||||
listing.Title, listing.Description, listing.Price,
|
||||
listing.Category, listing.ConditionNotes));
|
||||
}
|
||||
Persist(); // E1: propagates on failure so caller can show error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a source photo into the listing's export folder and returns the destination path.
|
||||
/// currentCount is the number of photos already in the edit session (used for naming).
|
||||
/// </summary>
|
||||
public string CopyPhotoToExportFolder(SavedListing listing, string sourcePath, int currentCount)
|
||||
{
|
||||
if (!Directory.Exists(listing.ExportFolder))
|
||||
throw new DirectoryNotFoundException($"Export folder not found: {listing.ExportFolder}");
|
||||
|
||||
var safeName = MakeSafeFilename(listing.Title);
|
||||
var ext = Path.GetExtension(sourcePath);
|
||||
|
||||
var dest = currentCount == 0
|
||||
? Path.Combine(listing.ExportFolder, $"{safeName}{ext}")
|
||||
: Path.Combine(listing.ExportFolder, $"{safeName}_{currentCount + 1}{ext}");
|
||||
|
||||
// Ensure unique filename if a file with that name already exists
|
||||
int attempt = 2;
|
||||
while (File.Exists(dest))
|
||||
{
|
||||
dest = Path.Combine(listing.ExportFolder, $"{safeName}_{currentCount + attempt}{ext}");
|
||||
attempt++;
|
||||
}
|
||||
|
||||
File.Copy(sourcePath, dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
// S3: use ProcessStartInfo with FileName so spaces/special chars are handled correctly
|
||||
public void OpenExportFolder(SavedListing listing)
|
||||
{
|
||||
|
||||
@@ -262,6 +262,15 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<Button Click="EditListing_Click"
|
||||
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
||||
Height="34" Padding="14,0" Margin="0,0,8,6">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<iconPacks:PackIconMaterial Kind="Pencil" Width="13" Height="13"
|
||||
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Edit" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Click="OpenFolderDetail_Click"
|
||||
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
||||
Height="34" Padding="14,0" Margin="0,0,8,6">
|
||||
@@ -311,6 +320,69 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Edit panel — shown in place of DetailPanel when editing -->
|
||||
<ScrollViewer x:Name="EditPanel" Visibility="Collapsed"
|
||||
VerticalScrollBarVisibility="Auto" Padding="18,14">
|
||||
<StackPanel>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="TITLE" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
|
||||
<TextBox x:Name="EditTitle" FontSize="13" Margin="0,0,0,4"
|
||||
mah:TextBoxHelper.Watermark="Listing title"/>
|
||||
|
||||
<!-- Price + Category -->
|
||||
<Grid Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="10"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="PRICE (£)" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
|
||||
<mah:NumericUpDown x:Name="EditPrice" Minimum="0" Maximum="99999"
|
||||
StringFormat="F2" Interval="0.5" Value="0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2">
|
||||
<TextBlock Text="CATEGORY" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
|
||||
<TextBox x:Name="EditCategory" FontSize="12"
|
||||
mah:TextBoxHelper.Watermark="e.g. Clothing, Shoes & Accessories"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Condition notes -->
|
||||
<TextBlock Text="CONDITION NOTES" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
|
||||
<TextBox x:Name="EditCondition" FontSize="12" Margin="0,0,0,4"
|
||||
mah:TextBoxHelper.Watermark="Optional — e.g. minor scuff on base"/>
|
||||
|
||||
<!-- Description -->
|
||||
<TextBlock Text="DESCRIPTION" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
|
||||
<TextBox x:Name="EditDescription" FontSize="12" Margin="0,0,0,4"
|
||||
TextWrapping="Wrap" AcceptsReturn="True"
|
||||
Height="130" VerticalScrollBarVisibility="Auto"/>
|
||||
|
||||
<!-- Photos -->
|
||||
<TextBlock Text="PHOTOS" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled"
|
||||
Margin="0,0,0,10">
|
||||
<StackPanel x:Name="EditPhotosPanel" Orientation="Horizontal"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Save / Cancel -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
|
||||
<Button x:Name="SaveEditBtn" Click="SaveEdit_Click"
|
||||
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
||||
Height="34" Padding="16,0" Margin="0,0,8,0"
|
||||
Content="Save Changes"/>
|
||||
<Button x:Name="CancelEditBtn" Click="CancelEdit_Click"
|
||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
||||
Height="34" Padding="14,0"
|
||||
Content="Cancel"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Imaging;
|
||||
using EbayListingTool.Models;
|
||||
using EbayListingTool.Services;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace EbayListingTool.Views;
|
||||
|
||||
@@ -14,6 +15,10 @@ public partial class SavedListingsView : UserControl
|
||||
private SavedListingsService? _service;
|
||||
private SavedListing? _selected;
|
||||
|
||||
// Edit mode working state
|
||||
private List<string> _editPhotoPaths = new();
|
||||
private List<string> _pendingDeletes = new();
|
||||
|
||||
// Normal card background — resolved once after load so we can restore it on mouse-leave
|
||||
private Brush? _cardNormalBg;
|
||||
private Brush? _cardHoverBg;
|
||||
@@ -327,6 +332,297 @@ public partial class SavedListingsView : UserControl
|
||||
catch { }
|
||||
}
|
||||
|
||||
// ---- Edit mode ----
|
||||
|
||||
private void EditListing_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_selected == null) return;
|
||||
EnterEditMode(_selected);
|
||||
}
|
||||
|
||||
private void EnterEditMode(SavedListing listing)
|
||||
{
|
||||
EditTitle.Text = listing.Title;
|
||||
EditPrice.Value = (double)listing.Price;
|
||||
EditCategory.Text = listing.Category;
|
||||
EditCondition.Text = listing.ConditionNotes;
|
||||
EditDescription.Text = listing.Description;
|
||||
|
||||
_editPhotoPaths = new List<string>(listing.PhotoPaths);
|
||||
_pendingDeletes = new List<string>();
|
||||
|
||||
BuildEditPhotoStrip();
|
||||
|
||||
DetailPanel.Visibility = Visibility.Collapsed;
|
||||
EditPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void ExitEditMode()
|
||||
{
|
||||
EditPanel.Visibility = Visibility.Collapsed;
|
||||
DetailPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void BuildEditPhotoStrip()
|
||||
{
|
||||
EditPhotosPanel.Children.Clear();
|
||||
|
||||
for (int i = 0; i < _editPhotoPaths.Count; i++)
|
||||
{
|
||||
var path = _editPhotoPaths[i];
|
||||
var index = i; // capture for lambdas
|
||||
|
||||
var container = new Grid { Width = 120, Height = 120, Margin = new Thickness(0, 0, 8, 0) };
|
||||
|
||||
// Photo image
|
||||
var imgBorder = new Border
|
||||
{
|
||||
Width = 120, Height = 120,
|
||||
CornerRadius = new CornerRadius(6),
|
||||
ClipToBounds = true,
|
||||
Background = (Brush)FindResource("MahApps.Brushes.Gray8")
|
||||
};
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.UriSource = new Uri(path, UriKind.Absolute);
|
||||
bmp.DecodePixelWidth = 240;
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
imgBorder.Child = new Image { Source = bmp, Stretch = Stretch.UniformToFill };
|
||||
}
|
||||
catch { AddPhotoIcon(imgBorder); }
|
||||
}
|
||||
else
|
||||
{
|
||||
AddPhotoIcon(imgBorder);
|
||||
}
|
||||
|
||||
container.Children.Add(imgBorder);
|
||||
|
||||
// "Cover" badge on the first photo
|
||||
if (i == 0)
|
||||
{
|
||||
var badge = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Background = (Brush)FindResource("MahApps.Brushes.Accent"),
|
||||
Padding = new Thickness(4, 1, 4, 1),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(4, 4, 0, 0)
|
||||
};
|
||||
badge.Child = new TextBlock { Text = "Cover", FontSize = 9, Foreground = Brushes.White };
|
||||
container.Children.Add(badge);
|
||||
}
|
||||
|
||||
// Remove (×) button — top-right corner
|
||||
var removeBtn = new Button
|
||||
{
|
||||
Content = "×",
|
||||
Width = 22, Height = 22,
|
||||
FontSize = 13,
|
||||
Padding = new Thickness(0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(0, 2, 2, 0),
|
||||
Style = (Style)FindResource("MahApps.Styles.Button.Square"),
|
||||
Foreground = Brushes.White,
|
||||
Background = new SolidColorBrush(Color.FromArgb(200, 40, 40, 40)),
|
||||
ToolTip = "Remove photo"
|
||||
};
|
||||
removeBtn.Click += (s, e) =>
|
||||
{
|
||||
_pendingDeletes.Add(_editPhotoPaths[index]);
|
||||
_editPhotoPaths.RemoveAt(index);
|
||||
BuildEditPhotoStrip();
|
||||
};
|
||||
container.Children.Add(removeBtn);
|
||||
|
||||
// Left/right reorder buttons — bottom-centre
|
||||
var reorderPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
Margin = new Thickness(0, 0, 0, 4)
|
||||
};
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
var leftBtn = new Button
|
||||
{
|
||||
Content = "◀",
|
||||
Width = 26, Height = 20,
|
||||
FontSize = 9,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0, 0, 2, 0),
|
||||
Style = (Style)FindResource("MahApps.Styles.Button.Square"),
|
||||
Foreground = Brushes.White,
|
||||
Background = new SolidColorBrush(Color.FromArgb(180, 40, 40, 40)),
|
||||
ToolTip = "Move left"
|
||||
};
|
||||
leftBtn.Click += (s, e) =>
|
||||
{
|
||||
(_editPhotoPaths[index], _editPhotoPaths[index - 1]) =
|
||||
(_editPhotoPaths[index - 1], _editPhotoPaths[index]);
|
||||
BuildEditPhotoStrip();
|
||||
};
|
||||
reorderPanel.Children.Add(leftBtn);
|
||||
}
|
||||
|
||||
if (i < _editPhotoPaths.Count - 1)
|
||||
{
|
||||
var rightBtn = new Button
|
||||
{
|
||||
Content = "▶",
|
||||
Width = 26, Height = 20,
|
||||
FontSize = 9,
|
||||
Padding = new Thickness(0),
|
||||
Style = (Style)FindResource("MahApps.Styles.Button.Square"),
|
||||
Foreground = Brushes.White,
|
||||
Background = new SolidColorBrush(Color.FromArgb(180, 40, 40, 40)),
|
||||
ToolTip = "Move right"
|
||||
};
|
||||
rightBtn.Click += (s, e) =>
|
||||
{
|
||||
(_editPhotoPaths[index], _editPhotoPaths[index + 1]) =
|
||||
(_editPhotoPaths[index + 1], _editPhotoPaths[index]);
|
||||
BuildEditPhotoStrip();
|
||||
};
|
||||
reorderPanel.Children.Add(rightBtn);
|
||||
}
|
||||
|
||||
if (reorderPanel.Children.Count > 0)
|
||||
container.Children.Add(reorderPanel);
|
||||
|
||||
EditPhotosPanel.Children.Add(container);
|
||||
}
|
||||
|
||||
// "Add photos" button at the end of the strip
|
||||
var addBtn = new Button
|
||||
{
|
||||
Width = 60, Height = 120,
|
||||
Style = (Style)FindResource("MahApps.Styles.Button.Square"),
|
||||
ToolTip = "Add photos"
|
||||
};
|
||||
var addContent = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
addContent.Children.Add(new MahApps.Metro.IconPacks.PackIconMaterial
|
||||
{
|
||||
Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.Plus,
|
||||
Width = 20, Height = 20,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Foreground = (Brush)FindResource("MahApps.Brushes.Accent")
|
||||
});
|
||||
addContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "Add",
|
||||
FontSize = 11,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Foreground = (Brush)FindResource("MahApps.Brushes.Accent"),
|
||||
Margin = new Thickness(0, 4, 0, 0)
|
||||
});
|
||||
addBtn.Content = addContent;
|
||||
addBtn.Click += AddEditPhoto_Click;
|
||||
EditPhotosPanel.Children.Add(addBtn);
|
||||
}
|
||||
|
||||
private void AddEditPhoto_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_selected == null || _service == null) return;
|
||||
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
Multiselect = true,
|
||||
Filter = "Image files|*.jpg;*.jpeg;*.png;*.gif;*.bmp|All files|*.*",
|
||||
Title = "Add Photos"
|
||||
};
|
||||
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
|
||||
foreach (var src in dlg.FileNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dest = _service.CopyPhotoToExportFolder(_selected, src, _editPhotoPaths.Count);
|
||||
_editPhotoPaths.Add(dest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Could not add \"{Path.GetFileName(src)}\": {ex.Message}",
|
||||
"Add Photo", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
BuildEditPhotoStrip();
|
||||
}
|
||||
|
||||
private void SaveEdit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_selected == null || _service == null) return;
|
||||
|
||||
var title = EditTitle.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
MessageBox.Show("Title cannot be empty.", "Save", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
EditTitle.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
_selected.Title = title;
|
||||
_selected.Price = (decimal)(EditPrice.Value ?? 0);
|
||||
_selected.Category = EditCategory.Text.Trim();
|
||||
_selected.ConditionNotes = EditCondition.Text.Trim();
|
||||
_selected.Description = EditDescription.Text.Trim();
|
||||
_selected.PhotoPaths = new List<string>(_editPhotoPaths);
|
||||
|
||||
try
|
||||
{
|
||||
_service.Update(_selected);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Failed to save changes:\n{ex.Message}", "Save Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete removed photos from disk now that the save succeeded
|
||||
foreach (var path in _pendingDeletes)
|
||||
{
|
||||
try { if (File.Exists(path)) File.Delete(path); }
|
||||
catch { /* ignore — file may already be gone */ }
|
||||
}
|
||||
_pendingDeletes.Clear();
|
||||
|
||||
ExitEditMode();
|
||||
ShowDetail(_selected, animate: false);
|
||||
RefreshList();
|
||||
}
|
||||
|
||||
private void CancelEdit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_selected != null)
|
||||
{
|
||||
// Delete any photos that were added to disk during this edit session but not saved
|
||||
var originalPaths = new HashSet<string>(_selected.PhotoPaths);
|
||||
foreach (var path in _editPhotoPaths.Where(p => !originalPaths.Contains(p)))
|
||||
{
|
||||
try { if (File.Exists(path)) File.Delete(path); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
_editPhotoPaths.Clear();
|
||||
_pendingDeletes.Clear();
|
||||
ExitEditMode();
|
||||
}
|
||||
|
||||
// ---- Button handlers ----
|
||||
|
||||
private void OpenExportsDir_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"Ebay": {
|
||||
"ClientId": "YOUR_EBAY_CLIENT_ID",
|
||||
"ClientSecret": "YOUR_EBAY_CLIENT_SECRET",
|
||||
"RuName": "YOUR_EBAY_RUNAME",
|
||||
"ClientId": "PeterFos-Lister-SBX-f6c15d8b1-1e21a7cf",
|
||||
"ClientSecret": "SBX-6c15d8b15850-bd12-45b9-a4d9-d5d7",
|
||||
"RuName": "Peter_Foster-PeterFos-Lister-eutksmb",
|
||||
"Sandbox": true,
|
||||
"RedirectPort": 8080,
|
||||
"DefaultPostcode": "NR1 1AA"
|
||||
|
||||
Reference in New Issue
Block a user