chore: folders

This commit is contained in:
2026-03-02 16:01:12 +01:00
parent 272ed999d8
commit a6112bec44
18 changed files with 29 additions and 30 deletions

View File

@@ -0,0 +1,106 @@
using System;
using System.Drawing;
namespace CarManagerV3
{
/// <summary>
/// The <c>ImageManager</c> class is responsible for managing car images, including fetching and storing them locally.
/// </summary>
internal class ImageManager
{
/// <summary>
/// Initializes the image folder by creating it if it does not exist.
/// </summary>
public static void InitializeImageFolder()
{
string path = "images";
if (!System.IO.Directory.Exists(path))
{
System.IO.Directory.CreateDirectory(path);
}
// Do not catch here. If we cannot create our images folder, the program wont work.
}
/// <summary>
/// Generates the image path for a given car based on its attributes.
/// </summary>
/// <param name="car">The car.</param>
/// <returns>The image path for this Car.</returns>
public static string GetImagePath(Car car)
{
string basePath = "images/";
string fileName = $"{car.Make}_{car.Model}_{car.Year}_{car.Color}.png";
return basePath + fileName;
}
/// <summary>
/// Gets the image for a given car, fetching it if necessary.
/// </summary>
/// <param name="car">The car.</param>
/// <returns>The <see cref="Image"/> object for the car.</returns>
public static Image GetImage(Car car)
{
InitializeImageFolder();
FetchImage(car);
string path = GetImagePath(car);
// does image exist?
try
{
if (System.IO.File.Exists(path))
{
return Image.FromFile(path);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error loading image: {ex.Message}");
}
try
{
return Image.FromFile("images/no_image_available.png");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error loading fallback image: {ex.Message}");
return null;
}
}
/// <summary>
/// Fetches an image for a Car from https://cdn.imagin.studio/getimage if it does not already exist locally and saves it to images/<c>Make_Model_Year_Color.webp</>.
/// If the image cannot be fetched, a placeholder image is used instead.
/// Uses example customer id "hrjavascript-mastery", because they wouldn't give me one. Stole this from a tutorial page. :)
/// </summary>
/// <param name="car">The car.</param>
public static void FetchImage(Car car)
{
// Fetch the image from https://cdn.imagin.studio/getimage and save it to images/Make_Model_Year.webp
// use params like this: ?customer=hrjavascript-mastery&zoomType=fullscreen&make={make}&modelFamily={model}&modelYear={year}&angle=front&paintDescription={color}&fileType=png
// check if the image already exists
string path = GetImagePath(car);
if (System.IO.File.Exists(path))
{
return;
}
string url = $"https://cdn.imagin.studio/getimage?customer=hrjavascript-mastery&zoomType=fullscreen&make={car.Make}&modelFamily={car.Model}&modelYear={car.Year}&angle=front&paintDescription={car.Color}&fileType=png";
//add Referer header to avoid 403 error
using (var client = new System.Net.WebClient())
{
client.Headers.Add("Referer", "http://localhost");
try
{
client.DownloadFile(url, path);
}
catch
{
// if error, use fallback image no_image_available.png
System.IO.File.Copy("images/no_image_available.png", path);
}
}
}
}
}

View File

@@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace CarManagerV3
{
/// <summary>
/// Handles safe reading and writing of car data to files.
/// </summary>
internal class SafeManager
{
/// <summary>
/// The path of the txt file that contains recently opened file paths.
/// </summary>
private static readonly string recentPathsFile = "recent_paths.txt";
/// <summary>
/// Initializes a file at a specified path if it does not already exist.
/// </summary>
/// <param name="path">The path.</param>
public static void InitializeFile(string path)
{
try
{
if (!File.Exists(@path))
{
using (StreamWriter writer = new StreamWriter(@path))
{
// Create the file, empty
writer.WriteLine();
writer.Close();
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error initializing file: {ex.Message}");
}
}
/// <summary>
/// Reads cars from a specified file path.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>
/// A <see cref="List{Car}"/> containing the cars read from the file.
/// </returns>
public static List<Car> ReadCars(string path)
{
List<Car> cars = new List<Car>();
List<string> failedLines = new List<string>();
bool isLegacy = false;
try
{
using (StreamReader reader = new StreamReader(@path))
{
string line;
while ((line = reader.ReadLine()) != null)
{
// Process the line
if (line == "") continue;
if (Car.isLegacyCsvString(line))
{
if (!StateManager.askForMigration())
{
MessageBox.Show("The file you are trying to open is in an old format that is no longer supported. Please select a different file.", "Unsupported Format", MessageBoxButtons.OK, MessageBoxIcon.Error);
throw new LegacyException();
//Environment.Exit(0);
}
else
{
isLegacy = true;
}
}
Car tmp = Car.FromCsvString(line);
if (tmp == null)
{
failedLines.Add(line);
continue;
}
cars.Add(tmp);
}
reader.Close();
}
}
catch (LegacyException ex)
{
throw ex;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error reading cars from file: {ex.Message}");
}
if (failedLines.Count > 0)
{
Console.Error.WriteLine($"Warning: {failedLines.Count} lines could not be parsed and were skipped.");
foreach (string failedLine in failedLines)
{
Console.Error.WriteLine($"Failed line: {failedLine}");
}
MessageBox.Show($"Warning: {failedLines.Count} lines in the file could not be parsed and were skipped. Check the console for details.", "Parsing Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
cars = StateManager.normalizeOrders(cars);
cars = cars.OrderBy(c => c.Order).ToList();
if (isLegacy)
{
SafeManager.SaveCars(path, cars);
}
return cars;
}
/// <summary>
/// Saves the cars to a specified file path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="cars">A <see cref="List{Car}"/> containing all cars to save.</param>
public static void SaveCars(string path, List<Car> cars)
{
try
{
using (StreamWriter writer = new StreamWriter(@path))
{
foreach (Car car in cars)
{
writer.WriteLine(car.ToCsvString());
}
writer.Close();
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error saving cars to file: {ex.Message}");
MessageBox.Show($"Error saving cars to file: {ex.Message}", "Save Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Adds a file path to the recent paths list.
/// If a path is already in the list, it is moved to the top.
/// Otherwise, it is added to the top.
/// Keeps only the 5 most recent paths.
/// </summary>
/// <param name="path">The path.</param>
public static void AddRecentPath(string path)
{
List<string> paths = new List<string>();
try
{
if (File.Exists(recentPathsFile))
{
using (StreamReader reader = new StreamReader(recentPathsFile))
{
string line;
while ((line = reader.ReadLine()) != null)
{
paths.Add(line);
}
reader.Close();
}
}
paths.Remove(path);
paths.Insert(0, path);
if (paths.Count > 5)
{
paths = paths.Take(5).ToList();
}
using (StreamWriter writer = new StreamWriter(recentPathsFile))
{
foreach (string p in paths)
{
writer.WriteLine(p);
}
writer.Close();
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error managing recent paths: {ex.Message}");
}
}
/// <summary>
/// Gets the recently opened file paths.
/// </summary>
/// <returns>
/// A <see cref="List{String}"/> containing the recent file paths.
/// </returns>
public static List<string> GetRecentPaths()
{
List<string> paths = new List<string>();
try
{
if (File.Exists(recentPathsFile))
{
using (StreamReader reader = new StreamReader(recentPathsFile))
{
string line;
while ((line = reader.ReadLine()) != null)
{
paths.Add(line);
}
reader.Close();
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error reading recent paths: {ex.Message}");
}
return paths;
}
/// <summary>
/// Gets the folder of the most recently opened file, or the users documents folder if no recent files.
/// </summary>
/// <returns></returns>
public static string getRecentFolder()
{
List<string> recentPaths = GetRecentPaths();
if (recentPaths.Count > 0)
{
string recentFile = recentPaths[0];
if (File.Exists(recentFile))
{
return Path.GetDirectoryName(recentFile);
}
}
return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace CarManagerV3
{
// Could be made non-static / non-singleton if multiple collections are needed in the future. Not likely though.
/// <summary>
/// The <c>StateManager</c> class is responsible for managing the state of the car collection, including adding, removing, updating, and retrieving cars.
/// Fully static / singleton at the moment, as only one collection is needed.
/// </summary>
internal class StateManager
{
// Initialize global static list of cars
static List<Car> cars = new List<Car>();
// Initialize default file path for car data.
// TODO: If no recent file paths are found, prompt user to select a file path instead of using a hardcoded default in the program folder.
static string filePath = "cars.csv";
static bool hasConfirmedMigration = false;
/// <summary>
/// Gets a car by its identifier.
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>
/// A <see cref="Car"/> object if found; otherwise, null.
/// </returns>
public static Car GetCarById(string id)
{
cars = SafeManager.ReadCars(filePath);
return cars.FirstOrDefault(c => c.Id == id);
}
/// <summary>
/// Public getter and setter for the cars list. Used to have better control over the list.
/// </summary>
/// <value>
/// The cars.
/// </value>
public static List<Car> Cars { get { return cars; } set { cars = value; } }
/// <summary>
/// Adds a car to the collection.
/// </summary>
/// <param name="car">The car to add.</param>
public static void AddCar(Car car)
{
cars = SafeManager.ReadCars(filePath);
cars.Add(car);
SafeManager.SaveCars(filePath, cars);
}
/// <summary>
/// Removes a car from the collection.
/// </summary>
/// <param name="car">The car to remove.</param>
public static void RemoveCar(Car car)
{
cars = SafeManager.ReadCars(filePath);
Car existingCar = GetCarById(car.Id);
if (existingCar == null) return;
cars.Remove(existingCar);
SafeManager.SaveCars(filePath, cars);
}
/// <summary>
/// Updates a car in the collection. Identifies itself by its Id.
/// </summary>
/// <remarks>
/// If the car's Id has changed during editing, this will not work correctly. Keep Id immutable!
/// </remarks>
/// <param name="car">The car to update.</param>
public static void UpdateCar(Car car)
{
Car existingCar = GetCarById(car.Id);
if (existingCar != null)
{
int index = cars.IndexOf(existingCar);
cars[index] = car;
Console.WriteLine("Updated car: " + existingCar.Id);
SafeManager.SaveCars(filePath, cars);
}
}
/// <summary>
/// Creates a new car and adds it to the collection.
/// </summary>
/// <param name="make">The make.</param>
/// <param name="model">The model.</param>
/// <param name="year">The year.</param>
/// <param name="color">The color.</param>
/// <param name="mileage">The mileage.</param>
/// <param name="price">The price.</param>
/// <returns>
/// The newly created <see cref="Car"/> object.
/// </returns>
public static Car CreateCar(string make, string model, int year, string color, int mileage, decimal price)
{
cars = SafeManager.ReadCars(filePath);
int newOrder = cars.Count > 0 ? cars.Max(c => c.Order) + 1 : 1;
Car newCar = new Car("", make, model, year, color, mileage, price, newOrder);
AddCar(newCar);
return newCar;
}
/// <summary>
/// Sets the file path used for saving and loading car data.
/// Called when user selects a new file path.
/// </summary>
/// <param name="path">The path.</param>
public static void setFilePath(string path)
{
// Reset migration confirmation when changing file path, as the new file may also require migration.
hasConfirmedMigration = false;
filePath = path;
}
public static List<Car> normalizeOrders(List<Car> cars)
{
// Normalize the Order field of all cars to be sequential starting from 1, while keeping the relative order the same.
var orderedCars = cars.OrderBy(c => c.Order).ToList();
for (int i = 0; i < orderedCars.Count; i++)
{
orderedCars[i].Order = i + 1;
}
return orderedCars;
}
public static bool askForMigration()
{
if (hasConfirmedMigration)
{
return true;
}
DialogResult result = MessageBox.Show("The file you are trying to open is in an older format. Do you want to attempt to migrate it to the new format? If you choose not to migrate, the file will not be opened.", "Migration Needed", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
hasConfirmedMigration = result == DialogResult.Yes;
return hasConfirmedMigration;
}
}
}