218 lines
8.0 KiB
C#
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})";
|
|
}
|
|
}
|