Files
EbayListingTool/EbayListingTool/Services/SavedListingsService.cs

218 lines
8.0 KiB
C#

using EbayListingTool.Models;
using Newtonsoft.Json;
namespace EbayListingTool.Services;
/// <summary>
/// Persists saved listings to %APPDATA%\EbayListingTool\saved_listings.json
/// and exports each listing to its own subfolder under
/// %APPDATA%\EbayListingTool\Exports\{ItemName}\
/// </summary>
public class SavedListingsService
{
private static readonly string AppDataDir =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"EbayListingTool");
private static readonly string ExportsDir = Path.Combine(AppDataDir, "Exports");
private static readonly string IndexFile = Path.Combine(AppDataDir, "saved_listings.json");
private List<SavedListing> _listings = new();
public IReadOnlyList<SavedListing> Listings => _listings;
public SavedListingsService()
{
Directory.CreateDirectory(AppDataDir);
Directory.CreateDirectory(ExportsDir);
Load();
}
/// <summary>
/// Saves the listing: copies photos to an export folder, writes a text file,
/// appends to the JSON index, and returns (listing, skippedPhotoCount).
/// </summary>
public (SavedListing Listing, int SkippedPhotos) Save(
string title, string description, decimal price,
string category, string conditionNotes,
IEnumerable<string> sourcePaths)
{
var safeName = MakeSafeFilename(title);
var exportDir = UniqueDir(Path.Combine(ExportsDir, safeName));
Directory.CreateDirectory(exportDir);
// Copy & rename photos — track skipped source files
var photoPaths = new List<string>();
var sources = sourcePaths.ToList();
for (int i = 0; i < sources.Count; i++)
{
var src = sources[i];
if (!File.Exists(src)) continue; // E3: track but don't silently ignore
var ext = Path.GetExtension(src);
var dest = i == 0
? Path.Combine(exportDir, $"{safeName}{ext}")
: Path.Combine(exportDir, $"{safeName}_{i + 1}{ext}");
File.Copy(src, dest, overwrite: true);
photoPaths.Add(dest);
}
// Write text file
var textFile = Path.Combine(exportDir, $"{safeName}.txt");
File.WriteAllText(textFile, BuildTextExport(title, description, price, category, conditionNotes));
var listing = new SavedListing
{
Title = title,
Description = description,
Price = price,
Category = category,
ConditionNotes = conditionNotes,
ExportFolder = exportDir,
PhotoPaths = photoPaths
};
_listings.Insert(0, listing); // newest first
Persist(); // E1: propagates on failure so caller can show error
return (listing, sources.Count - photoPaths.Count);
}
public void Delete(SavedListing listing)
{
_listings.Remove(listing);
Persist();
try
{
if (Directory.Exists(listing.ExportFolder))
Directory.Delete(listing.ExportFolder, recursive: true);
}
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)
{
var dir = Directory.Exists(listing.ExportFolder) ? listing.ExportFolder : ExportsDir;
if (Directory.Exists(dir))
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = dir,
UseShellExecute = true
});
}
// ---- Private helpers ----
private void Load()
{
if (!File.Exists(IndexFile)) return;
try
{
var json = File.ReadAllText(IndexFile);
_listings = JsonConvert.DeserializeObject<List<SavedListing>>(json) ?? new();
}
catch (Exception ex)
{
// E2: back up corrupt index rather than silently discarding all records
var backup = IndexFile + ".corrupt." + DateTime.Now.ToString("yyyyMMddHHmmss");
try { File.Move(IndexFile, backup); } catch { /* can't backup, proceed */ }
System.Diagnostics.Debug.WriteLine(
$"SavedListingsService: index corrupt, backed up to {backup}. Error: {ex.Message}");
_listings = new();
}
}
private void Persist()
{
// E1: let exceptions propagate so callers can surface them to the user
var json = JsonConvert.SerializeObject(_listings, Formatting.Indented);
File.WriteAllText(IndexFile, json);
}
private static string BuildTextExport(string title, string description,
decimal price, string category,
string conditionNotes)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Title: {title}");
sb.AppendLine($"Category: {category}");
sb.AppendLine($"Price: £{price:F2}");
if (!string.IsNullOrWhiteSpace(conditionNotes))
sb.AppendLine($"Condition: {conditionNotes}");
sb.AppendLine();
sb.AppendLine("Description:");
sb.AppendLine(description);
return sb.ToString();
}
private static string MakeSafeFilename(string name)
{
// S2: replace invalid chars, then strip trailing dots/spaces Windows silently removes
var invalid = Path.GetInvalidFileNameChars();
var safe = string.Join("", name.Select(c => invalid.Contains(c) ? '_' : c)).Trim();
if (safe.Length > 80) safe = safe[..80];
safe = safe.TrimEnd('.', ' ');
return safe.Length > 0 ? safe : "Listing";
}
private static string UniqueDir(string path)
{
if (!Directory.Exists(path)) return path;
int i = 2;
while (Directory.Exists($"{path} ({i})")) i++;
return $"{path} ({i})";
}
}