diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..548f21c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/PcgsInvUi/[Bb]in/ +/PcgsInvUi/[Oo]bj/ +[Oo]bj/ +[Bb]in/ diff --git a/.idea/.idea.pcgs-inv/.idea/avalonia.xml b/.idea/.idea.pcgs-inv/.idea/avalonia.xml index 2e6f308..091e888 100644 --- a/.idea/.idea.pcgs-inv/.idea/avalonia.xml +++ b/.idea/.idea.pcgs-inv/.idea/avalonia.xml @@ -6,6 +6,8 @@ + + diff --git a/PcgsInvUi/App.axaml b/PcgsInvUi/App.axaml index a175947..0855646 100644 --- a/PcgsInvUi/App.axaml +++ b/PcgsInvUi/App.axaml @@ -2,13 +2,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="PcgsInvUi.App" xmlns:local="using:PcgsInvUi" - RequestedThemeVariant="Dark"> - + xmlns:pcgsInvUi="clr-namespace:PcgsInvUi" + RequestedThemeVariant="Light"> + - + - + diff --git a/PcgsInvUi/App.axaml.cs b/PcgsInvUi/App.axaml.cs index 42647b1..e71a99c 100644 --- a/PcgsInvUi/App.axaml.cs +++ b/PcgsInvUi/App.axaml.cs @@ -1,32 +1,36 @@ +using System; +using System.Data.Entity.Migrations.Model; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using PcgsInvUi.Services; using PcgsInvUi.ViewModels; using PcgsInvUi.Views; +using PcgsInvUi.Models; namespace PcgsInvUi; -public partial class App : Application -{ - public override void Initialize() - { +public partial class App : Application { + public override void Initialize() { AvaloniaXamlLoader.Load(this); } - public override void OnFrameworkInitializationCompleted() - { + public override void OnFrameworkInitializationCompleted() { base.OnFrameworkInitializationCompleted(); - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - var coins = new Services.CoinCollection(); - - desktop.MainWindow = new MainWindow - { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + var coins = new CoinDatabase(); + desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel(coins), }; + + desktop.Exit += (sender, args) => { + coins.UpdateCollection("CollectionTable"); + }; } - base.OnFrameworkInitializationCompleted(); } + + public void OnExit(object sender, ControlledApplicationLifetimeExitEventArgs e) { + // Save the database? + } } \ No newline at end of file diff --git a/PcgsInvUi/Models/Coin.cs b/PcgsInvUi/Models/Coin.cs index fb045a9..2d9e8bf 100644 --- a/PcgsInvUi/Models/Coin.cs +++ b/PcgsInvUi/Models/Coin.cs @@ -1,135 +1,165 @@ -using System; -using ReactiveUI; +using ReactiveUI; namespace PcgsInvUi.Models; -public class Coin : ReactiveObject -{ - public int PcgsNumber - { - get => _pcgsNumber; - set => this.RaiseAndSetIfChanged(ref _pcgsNumber, value); +public class Coin : ReactiveObject { + public string PCGSNo { + get => _pcgsNo; + set => this.RaiseAndSetIfChanged(ref _pcgsNo, value); } - private int _pcgsNumber; - public string Name - { + private string _pcgsNo; + + public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); } + private string _name; - public int Year - { + public int Year { get => _year; set => this.RaiseAndSetIfChanged(ref _year, value); } + private int _year; - public string Denomination - { + public string Denomination { get => _denomination; set => this.RaiseAndSetIfChanged(ref _denomination, value); } + private string _denomination; - public string MintMark - { + public string MintMark { get => _mintMark; set => this.RaiseAndSetIfChanged(ref _mintMark, value); } + private string _mintMark; - public string Grade - { + public string Grade { get => _grade; set => this.RaiseAndSetIfChanged(ref _grade, value); } + private string _grade; - public double PriceGuideValue - { + public double PriceGuideValue { get => _priceGuideValue; - set => this.RaiseAndSetIfChanged(ref _priceGuideValue, value); + set { + TotalPrice = value * Quantity; + this.RaiseAndSetIfChanged(ref _priceGuideValue, value); + } } + private double _priceGuideValue; - public string CoinFactsLink - { + public string CoinFactsLink { get => _coinFactsLink; set => this.RaiseAndSetIfChanged(ref _coinFactsLink, value); } + private string _coinFactsLink; - public string MajorVariety - { + public string MajorVariety { get => _majorVariety; set => this.RaiseAndSetIfChanged(ref _majorVariety, value); } + private string _majorVariety; - public string MinorVariety - { + public string MinorVariety { get => _minorVariety; set => this.RaiseAndSetIfChanged(ref _minorVariety, value); } + private string _minorVariety; - public string DieVariety - { + public string DieVariety { get => _dieVariety; set => this.RaiseAndSetIfChanged(ref _dieVariety, value); } + private string _dieVariety; - public string Series - { - get => _series; - set => this.RaiseAndSetIfChanged(ref _series, value); + public string SeriesName { + get => _seriesName; + set => this.RaiseAndSetIfChanged(ref _seriesName, value); } - private string _series; - public string Category - { + private string _seriesName; + + public string Category { get => _category; set => this.RaiseAndSetIfChanged(ref _category, value); } + private string _category; - public string Designation - { + public string Designation { get => _designation; set => this.RaiseAndSetIfChanged(ref _designation, value); } + private string _designation; - public int CertificateNumber - { + public int CertificateNumber { get => _certificateNumber; set => this.RaiseAndSetIfChanged(ref _certificateNumber, value); } + private int _certificateNumber; - public int Quantity - { + public int Quantity { get => _quantity; - set => this.RaiseAndSetIfChanged(ref _quantity, value); + set { + TotalPrice = value * PriceGuideValue; + this.RaiseAndSetIfChanged(ref _quantity, value); + } } - private int _quantity; - - public double TotalPrice => Quantity * PriceGuideValue; - public double PaidFor - { + private int _quantity; + + public double TotalPrice { + get => _totalPrice; + set => this.RaiseAndSetIfChanged(ref _totalPrice, value); + } + + private double _totalPrice; + + public double PaidFor { get => _paidFor; set => this.RaiseAndSetIfChanged(ref _paidFor, value); } + private double _paidFor; - public String Notes - { + public string Notes { get => _notes; set => this.RaiseAndSetIfChanged(ref _notes, value); } + private string _notes; + + public Coin() { + PCGSNo = ""; + Name = ""; + Year = 0; + Denomination = ""; + MintMark = ""; + Grade = ""; + PriceGuideValue = 0; + CoinFactsLink = ""; + MajorVariety = ""; + MinorVariety = ""; + DieVariety = ""; + SeriesName = ""; + Category = ""; + Designation = ""; + CertificateNumber = 0; + Quantity = 0; + PaidFor = 0; + Notes = ""; + } } \ No newline at end of file diff --git a/PcgsInvUi/PcgsInvUi.csproj b/PcgsInvUi/PcgsInvUi.csproj index 2736751..5faf21a 100644 --- a/PcgsInvUi/PcgsInvUi.csproj +++ b/PcgsInvUi/PcgsInvUi.csproj @@ -1,26 +1,93 @@  - - WinExe - net6.0 - enable - true - app.manifest - true - + + WinExe + net6.0 + enable + true + app.manifest + true + - - - + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PcgsInvUi/Program.cs b/PcgsInvUi/Program.cs index 177ca67..cc1d9e7 100644 --- a/PcgsInvUi/Program.cs +++ b/PcgsInvUi/Program.cs @@ -1,17 +1,16 @@ using Avalonia; using Avalonia.ReactiveUI; using System; +using PcgsInvUi.ViewModels; namespace PcgsInvUi; -class Program -{ +class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() @@ -20,4 +19,4 @@ class Program .WithInterFont() .LogToTrace() .UseReactiveUI(); -} +} \ No newline at end of file diff --git a/PcgsInvUi/Services/CoinCollection.cs b/PcgsInvUi/Services/CoinCollection.cs index 7852adf..dc89845 100644 --- a/PcgsInvUi/Services/CoinCollection.cs +++ b/PcgsInvUi/Services/CoinCollection.cs @@ -3,10 +3,8 @@ using PcgsInvUi.Models; namespace PcgsInvUi.Services; -public class CoinCollection -{ - public IEnumerable GetItems() => new[] - { +public class CoinCollection { + public IEnumerable GetItems() => new[] { new Coin(), new Coin(), new Coin(), diff --git a/PcgsInvUi/Services/CoinDatabase.cs b/PcgsInvUi/Services/CoinDatabase.cs new file mode 100644 index 0000000..b954ef3 --- /dev/null +++ b/PcgsInvUi/Services/CoinDatabase.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.ObjectModel; +using System.Data.SQLite; +using System.Threading.Tasks; +using PcgsInvUi.Models; + +namespace PcgsInvUi.Services; + +public class CoinDatabase { + public SQLiteConnection Connection { get; set; } + private Boolean IsConnected { get; set; } + public ObservableCollection Collection { get; set; } + + private PcgsClient _pcgsClient; + + public CoinDatabase() { + // Create a connection to SQLite + Connection = CreateConnection(); + // Create a client to access the API. + _pcgsClient = new PcgsClient( + "eAb8gS0I2XAvT_5gJeiGJaglMia1Tk-oB4kJUK6kuafyrny_S61vIJY-Ikl4nCQM67wrdxzUqLVWTV2kBSxD3d5XNBHxHnYBhcSS6dOPug0hZaF3qAv56df3gYSzOGh9Tif5y0eP3Iw0LrqKDr1Hj-dk6SV6GKog2IIqCPQhhHH8FMTWBTYO-_O8cx7qLdM5GM8KlTsic6g3VRUhM8EA_4OO04dCfmNLGhqINRl3jGZ0Q4ziI8fng2bVWsIyteqiPzUn10rIQ3-OPpqVZG_DxeOmOejj4GzbUNyqUOajy-nr5rYY"); + // Create a base table. + CreateCollection("CollectionTable"); + Console.WriteLine("Connected to database."); + Collection = new ObservableCollection(); + GetCollection("CollectionTable"); + Console.WriteLine("Got collection."); + } + + ~CoinDatabase() { + Console.WriteLine("Destructor called, saving collection."); + // Save collection. + UpdateCollection("CollectionTable"); + // Close the connection. + Connection.Close(); + } + + private SQLiteConnection CreateConnection() { + // Check if database exists. + if (!System.IO.File.Exists("%USERPROFILE%\\coin_collection")) { + // Create the database. + SQLiteConnection.CreateFile("%USERPROFILE%\\coin_collection"); + } + + // Create a connection to the database. + SQLiteConnection connection = + new SQLiteConnection("DataSource=%USERPROFILE%\\coin_collection"); + + // Open the connection. + try { + connection.Open(); + IsConnected = true; + } + catch (Exception e) { + Console.WriteLine(e.Message); + } + + return connection; + } + + public void CreateCollection(string collectionName) { + // Create a table to store the collection. + if (!IsConnected) { + // TODO Create a NotConnectedToDatabaseException + Console.WriteLine("Not connected to database."); + return; + } + + // Create a SQL command. + SQLiteCommand cmd = Connection.CreateCommand(); + cmd.CommandText = + $"CREATE TABLE IF NOT EXISTS {collectionName} (" + + "PCGS_NUMBER TEXT," + + "GRADE TEXT" + + "NAME TEXT," + + "YEAR INTEGER," + + "DENOMINATION TEXT," + + "MINT_MARK TEXT," + + "PRICE_GUIDE_VALUE REAL," + + "COIN_FACTS_LINK TEXT," + + "MAJOR_VARIETY TEXT," + + "MINOR_VARIETY TEXT," + + "DIE_VARIETY TEXT," + + "SERIES_NAME TEXT," + + "CATEGORY TEXT," + + "DESIGNATION TEXT," + + "CERT_NUM INTEGER," + + "QUANTITY INTEGER," + + "NOTES TEXT," + + "PRIMARY KEY(PCGS_NUMBER, GRADE))"; + + // Execute the command. + cmd.ExecuteNonQuery(); + } + + public void InsertCoin(Coin coin) { + Console.WriteLine("Inserting coin..."); + // Use our existing connection to insert a coin to the database. + if (!IsConnected) { + // TODO Create a NotConnectedToDatabaseException + Console.WriteLine("Not connected to database."); + return; + } + + // Create a SQL command. + SQLiteCommand cmd = Connection.CreateCommand(); + cmd.CommandText = + "INSERT INTO CollectionTable (PCGS_NUMBER, GRADE, YEAR," + + "DENOMINATION, MINT_MARK, PRICE_GUIDE_VALUE, COIN_FACTS_LINK," + + "MAJOR_VARIETY, MINOR_VARIETY, DIE_VARIETY, SERIES_NAME, CATEGORY, DESIGNATION," + + "CERT_NUM, QUANTITY, NOTES) " + + $"VALUES ('{coin.PCGSNo}', '{coin.Grade}', {coin.Year}, '{coin.Denomination}', '{coin.MintMark}', {coin.PriceGuideValue}," + + $"'{coin.CoinFactsLink}', '{coin.MajorVariety}', '{coin.MinorVariety}', '{coin.DieVariety}', '{coin.SeriesName}', " + + $"'{coin.Category}', '{coin.Designation}', {coin.CertificateNumber}, {coin.Quantity}, @Notes)"; + cmd.Parameters.AddWithValue("@Notes", coin.Notes); + + // Execute the command. + cmd.ExecuteNonQuery(); + Console.WriteLine("Coin inserted."); + } + + public async Task CreateCoin(int pcgsNumber, string grade, int quantity) { + Console.WriteLine("Creating coin..."); + // Create a coin by calling the API, then insert it into the database. + // First, check if the coin already exists - if it does, just update the quantity by adding the amount entered. + var coin = GetCoin(pcgsNumber, grade); + if (coin is not null) { + coin.Quantity += quantity; + SaveCoin("CollectionTable", coin); + return PcgsClient.ErrorType.None; + } + + // Otherwise, build a new coin with the API and insert it. + var apiResult = await _pcgsClient.GetCoinFactsByGrade(pcgsNumber, grade); + if (apiResult.Item1 == PcgsClient.ErrorType.None) { + var newCoin = apiResult.Item2; + newCoin.Quantity = quantity; + InsertCoin(newCoin); + Collection.Add(newCoin); + return PcgsClient.ErrorType.None; + } + + Console.WriteLine("Error getting coin from API - {0}", apiResult.Item1); + return apiResult.Item1; + } + + public Coin? GetCoin(int pcgsNumber, string grade) { + Console.WriteLine($"Getting coin {pcgsNumber}, {grade}..."); + int gradeInt = GetGradeFromString(grade); + + // Check the collection to see if the coin exists. + // We can't make a more efficient algorithm since there's not really an ordering to the collection. + foreach (var coin in Collection) { + if (coin.PCGSNo == pcgsNumber.ToString() && coin.Grade.Contains(gradeInt.ToString())) { // This has the possibility of a false positive for single digit grades. + if (gradeInt < 10) + if (char.IsDigit(coin.Grade[3])) continue; // TODO For right now, we're going to assume that the first 2 characters are ciruclation strings, and the next 2 are grade numbers. + Console.WriteLine("Found coin."); + return coin; + } + } + + Console.WriteLine("Couldn't find coin."); + return null; + } + + public void GetCollection(string collectionName) { + Console.WriteLine("Getting collection..."); + SQLiteCommand cmd = Connection.CreateCommand(); + cmd.CommandText = $"SELECT * FROM {collectionName}"; + + using (var reader = cmd.ExecuteReader()) { + while (reader.Read()) { + // Create coin from reader then add to list. + Collection.Add(CoinFromReader(reader)); + } + } + } + + public String CollectionToCSV(string collectionName) { + var cmd = Connection.CreateCommand(); + cmd.CommandText = $"SELECT * FROM {collectionName}"; + + var headersCommand = Connection.CreateCommand(); + headersCommand.CommandText = $"PRAGMA table_info({collectionName})"; + + var resultString = ""; + var numHeaders = 0; + + // Get the headers. + using (var reader = headersCommand.ExecuteReader()) { + while (reader.Read()) { + resultString += reader.GetString(1) + ","; + numHeaders++; + } + } + + resultString += "\n"; + + // Is there a more efficient way of doing this? This is running at O(n^2) time. + using (var reader = cmd.ExecuteReader()) { + while (reader.Read()) { + // Read information from the reader, then add to CSV string. + for (int i = 0; i < numHeaders; i++) { + // Get the type of each column. + var type = reader.GetFieldType(i); + Console.WriteLine("Type: " + type); + if (type == typeof(Int64)) { + resultString += reader.GetInt32(i) + ","; + } + else if (type == typeof(Double)) { + resultString += reader.GetDouble(i) + ","; + } + else if (type == typeof(String)) { + resultString += reader.GetString(i) + ","; + } + else { + resultString += "NULL" + ","; + } + } + resultString += "\n"; + } + } + + return resultString; + } + + public void SaveCoin(string collectionName, Coin coin) { + // Get and replace the coin from the database. + Console.WriteLine("Saving coin..."); + SQLiteCommand cmd = Connection.CreateCommand(); + cmd.CommandText = $"UPDATE {collectionName} " + + $"SET YEAR = {coin.Year}, " + + $"DENOMINATION = \'{coin.Denomination}\', " + + $"MINT_MARK = \'{coin.MintMark}\', " + + $"PRICE_GUIDE_VALUE = \'{coin.PriceGuideValue}\', " + + $"COIN_FACTS_LINK = \'{coin.CoinFactsLink}\', " + + $"MAJOR_VARIETY = \'{coin.MajorVariety}\', " + + $"MINOR_VARIETY = \'{coin.MinorVariety}\', " + + $"DIE_VARIETY = \'{coin.DieVariety}\', " + + $"SERIES_NAME = \'{coin.SeriesName}\', " + + $"CATEGORY = \'{coin.Category}\', " + + $"DESIGNATION = \'{coin.Designation}\', " + + $"CERT_NUM = \'{coin.CertificateNumber}\', " + + $"QUANTITY = {coin.Quantity}, " + + $"NOTES = @Notes " + + $"WHERE PCGS_NUMBER = {coin.PCGSNo} AND GRADE = \'{coin.Grade}\'"; + cmd.Parameters.AddWithValue("@Notes", coin.Notes); + Console.WriteLine(cmd.CommandText); + // Execute the command. + cmd.ExecuteNonQuery(); + } + + public void UpdateCollection(string collectionName) { + // Update the database with the contents of the collection. + // TODO This is a very inefficient way of doing this. I should be able to do this in one command. + foreach (var coin in Collection) { + SaveCoin(collectionName, coin); // CAREFUL! This will not create a coin if it doesn't currently exist. + } + } + + private Coin CoinFromReader(SQLiteDataReader reader) { + // Create a coin while filtering out any possible null values - they throw exceptions. + Coin coin = new Coin(); + if (!reader.IsDBNull(0)) coin.PCGSNo = reader.GetString(0); + if (!reader.IsDBNull(1)) coin.Grade = reader.GetString(1); + if (!reader.IsDBNull(2)) coin.Year = reader.GetInt32(2); + if (!reader.IsDBNull(3)) coin.Denomination = reader.GetString(3); + if (!reader.IsDBNull(4)) coin.MintMark = reader.GetString(4); + if (!reader.IsDBNull(5)) coin.PriceGuideValue = reader.GetDouble(5); + if (!reader.IsDBNull(6)) coin.CoinFactsLink = reader.GetString(6); + if (!reader.IsDBNull(7)) coin.MajorVariety = reader.GetString(7); + if (!reader.IsDBNull(8)) coin.MinorVariety = reader.GetString(8); + if (!reader.IsDBNull(9)) coin.DieVariety = reader.GetString(9); + if (!reader.IsDBNull(10)) coin.SeriesName = reader.GetString(10); + if (!reader.IsDBNull(11)) coin.Category = reader.GetString(11); + if (!reader.IsDBNull(12)) coin.Designation = reader.GetString(12); + if (!reader.IsDBNull(13)) coin.CertificateNumber = reader.GetInt32(13); + if (!reader.IsDBNull(14)) coin.Quantity = reader.GetInt32(14); + if (!reader.IsDBNull(15)) coin.Notes = reader.GetString(15); + coin.TotalPrice = coin.Quantity * coin.PriceGuideValue; + return coin; + } + + public void DeleteCoin(Coin delCoin) { + SQLiteCommand cmd = Connection.CreateCommand(); + cmd.CommandText = "DELETE FROM CollectionTable WHERE PCGS_NUMBER = @PcgsNumber AND GRADE = @Grade"; + cmd.Parameters.AddWithValue("@PcgsNumber", delCoin.PCGSNo); + cmd.Parameters.AddWithValue("@Grade", delCoin.Grade); + + cmd.ExecuteNonQuery(); + } + + private int GetGradeFromString(string grade) { + var actualGrade = int.Parse(grade.Substring(grade.Length - 2)); + Console.WriteLine($"Grade is {actualGrade}"); + return actualGrade; + } +} \ No newline at end of file diff --git a/PcgsInvUi/Services/Database.cs b/PcgsInvUi/Services/Database.cs deleted file mode 100644 index 9dc4a04..0000000 --- a/PcgsInvUi/Services/Database.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PcgsInvUi.Services; - -public class Database -{ - // TODO Create a database using SQLite. -} \ No newline at end of file diff --git a/PcgsInvUi/Services/PcgsClient.cs b/PcgsInvUi/Services/PcgsClient.cs index b6bf2a7..e4329d2 100644 --- a/PcgsInvUi/Services/PcgsClient.cs +++ b/PcgsInvUi/Services/PcgsClient.cs @@ -1,51 +1,89 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using Newtonsoft.Json; using PcgsInvUi.Models; +using System.Text.Json; +using DynamicData.Kernel; +using ReactiveUI; namespace PcgsInvUi.Services; -public class PcgsClient -{ +public class PcgsClient : ReactiveObject { + public enum ErrorType { + None, + NoCoinFound, + InvalidRequestFormat, + ApiKeyInvalid + } + + public int NumApiRequests { + get => _numApiRequests; + set => this.RaiseAndSetIfChanged(ref _numApiRequests, value); + } + private string _apiUri = "https://api.pcgs.com/publicapi"; private readonly string _apiKey; + private int _numApiRequests; - public PcgsClient(string apiKey) - { - _apiKey = apiKey; + public PcgsClient(string apiKey) { + // _apiKey = apiKey; + _apiKey = + "E8xivTVN_k4Q8ubk8Xv_uU11U91WX0wDht4S4m4BAf2HPmKnGrZ0J2f9nccM7Ns1VkGoT1SXYR74I7aNaMKqXlJGEy3oobseKV0nGiqu1IKvFkOTVjhV37SWUVExSDFMC6FLzs7xJQIHwkcxr5JFOQNmc4ZytEh_-hmCN3NL-z0fEk98tzBmPJ_F7bq_xNru86QMD2A9yalu_dWe-lCysfyE2bCA2EDqwvWenRYf-0XnNtp6qlxxCMlCd-TDqqRKLC3qefCwpXSemwBiNSP5gbC9pwcKrZugkruuJ0nyxxThZymm"; } - public async Task GetCoinFactsByGrade(int pcgsNumber, int grade, bool plusGrade) - { + public async Task<(ErrorType, Coin?)> GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) { Coin newCoin = null; - using (HttpClient client = new HttpClient()) - { - string requestUri = - $"{_apiUri}/coindetail/GetCoinFactsByGrade/?PCGSNo={pcgsNumber}&GradeNo={grade}&PlusGrade={plusGrade}"; - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", _apiKey); - HttpResponseMessage response = await client.GetAsync(requestUri); + using (HttpClient client = new HttpClient()) { + var gradeParam = GetGradeFromString(grade); - if (response.IsSuccessStatusCode) - { + string requestUri = + $"{_apiUri}/coindetail/GetCoinFactsByGrade/?PCGSNo={pcgsNumber}&GradeNo={gradeParam}&PlusGrade={plusGrade}"; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", _apiKey); + Console.WriteLine($"Sending request to {requestUri}"); + HttpResponseMessage response = await client.GetAsync(requestUri); + Console.WriteLine("Got response."); + + if (response.IsSuccessStatusCode) { string jsonResponse = await response.Content.ReadAsStringAsync(); - newCoin = ParseJsonResponse(jsonResponse); - } - else - { - Console.WriteLine($"Error: {response.StatusCode} - {response.ReasonPhrase}"); + var parsedResponse = ParseJsonResponse(jsonResponse); + return parsedResponse; } } - - return newCoin; - } + return (ErrorType.ApiKeyInvalid, null); + } + + private int GetGradeFromString(string grade) { + var actualGrade = int.Parse(grade.Substring(grade.Length - 2)); + Console.WriteLine($"Grade is {actualGrade}"); + return actualGrade; + } + // TODO Will eventually create the other methods that are included in the Public API. - private Coin ParseJsonResponse(string json) - { - Coin newCoin = JsonConvert.DeserializeObject(json); - return newCoin; + private (ErrorType, Coin?) ParseJsonResponse(string json) { + Console.WriteLine(json); + // TODO Check for errors in the response + if (json.Contains("\"ServerMessage\":\"No data found\"")) return (ErrorType.NoCoinFound, null); + if (json.Contains("\"IsValidRequest\":false")) return (ErrorType.InvalidRequestFormat, null); + + var newCoin = JsonSerializer.Deserialize(json); + return (ErrorType.None, newCoin); } +} + +// Exception for invalid request format +class InvalidRequestFormatException : Exception { + public InvalidRequestFormatException() { } + public InvalidRequestFormatException(string message) : base(message) { } + public InvalidRequestFormatException(string message, Exception inner) : base(message, inner) { } +} + +// Exception for no coin found +class CoinNotFoundException : Exception { + public CoinNotFoundException() { } + public CoinNotFoundException(string message) : base(message) { } + public CoinNotFoundException(string message, Exception inner) : base(message, inner) { } } \ No newline at end of file diff --git a/PcgsInvUi/ViewLocator.cs b/PcgsInvUi/ViewLocator.cs index 8ee75d9..b80701a 100644 --- a/PcgsInvUi/ViewLocator.cs +++ b/PcgsInvUi/ViewLocator.cs @@ -5,23 +5,19 @@ using PcgsInvUi.ViewModels; namespace PcgsInvUi; -public class ViewLocator : IDataTemplate -{ - public Control Build(object data) - { +public class ViewLocator : IDataTemplate { + public Control Build(object data) { var name = data.GetType().FullName!.Replace("ViewModel", "View"); var type = Type.GetType(name); - if (type != null) - { + if (type != null) { return (Control)Activator.CreateInstance(type)!; } - + return new TextBlock { Text = "Not Found: " + name }; } - public bool Match(object data) - { + public bool Match(object data) { return data is ViewModelBase; } } \ No newline at end of file diff --git a/PcgsInvUi/ViewModels/CoinListViewModel.cs b/PcgsInvUi/ViewModels/CoinListViewModel.cs index 19fb367..a8285fa 100644 --- a/PcgsInvUi/ViewModels/CoinListViewModel.cs +++ b/PcgsInvUi/ViewModels/CoinListViewModel.cs @@ -5,19 +5,18 @@ using ReactiveUI; namespace PcgsInvUi.ViewModels; -public class CoinListViewModel : ViewModelBase -{ +public class CoinListViewModel : ViewModelBase { public ObservableCollection CoinCollection { get; set; } private float _totalValue; - public float TotalValue - { + + public float TotalValue { get => _totalValue; set => this.RaiseAndSetIfChanged(ref _totalValue, value); } + public Coin? SelectedCoin { get; set; } - - public CoinListViewModel(IEnumerable items) - { + + public CoinListViewModel(IEnumerable items) { CoinCollection = new ObservableCollection(items); TotalValue = 0; } diff --git a/PcgsInvUi/ViewModels/DeleteWindowViewModel.cs b/PcgsInvUi/ViewModels/DeleteWindowViewModel.cs new file mode 100644 index 0000000..54623cf --- /dev/null +++ b/PcgsInvUi/ViewModels/DeleteWindowViewModel.cs @@ -0,0 +1,15 @@ +using System.Reactive; +using PcgsInvUi.Views; +using ReactiveUI; + +namespace PcgsInvUi.ViewModels; + +public class DeleteWindowViewModel : ViewModelBase { + public ReactiveCommand OkCommand { get; } + public ReactiveCommand CancelCommand { get; } + + public DeleteWindowViewModel() { + OkCommand = ReactiveCommand.Create(() => (object)true); + CancelCommand = ReactiveCommand.Create(window => window.Close()); + } +} \ No newline at end of file diff --git a/PcgsInvUi/ViewModels/EditViewModel.cs b/PcgsInvUi/ViewModels/EditViewModel.cs index a8b1fd5..3572169 100644 --- a/PcgsInvUi/ViewModels/EditViewModel.cs +++ b/PcgsInvUi/ViewModels/EditViewModel.cs @@ -5,25 +5,23 @@ using ReactiveUI; namespace PcgsInvUi.ViewModels; -public class EditViewModel : ViewModelBase -{ +public class EditViewModel : ViewModelBase { public Coin CurrentCoin { get; set; } - + public ReactiveCommand OkCommand { get; } - public EditViewModel(Coin coin) - { + public EditViewModel(Coin coin) { CurrentCoin = coin; var okEnabled = this.WhenAnyValue( - x => x.CurrentCoin.PcgsNumber, + x => x.CurrentCoin.PCGSNo, x => x.CurrentCoin.Grade, x => x.CurrentCoin.Quantity, - (id, grade, quantity) => (id != 0) && + (id, grade, quantity) => !string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(grade) && (quantity != 0) ); - + // Setup OkCommand - all Coin-related properties must be non-whitespace. OkCommand = ReactiveCommand.Create(() => { }); } diff --git a/PcgsInvUi/ViewModels/ErrorWindowViewModel.cs b/PcgsInvUi/ViewModels/ErrorWindowViewModel.cs new file mode 100644 index 0000000..6d93ac2 --- /dev/null +++ b/PcgsInvUi/ViewModels/ErrorWindowViewModel.cs @@ -0,0 +1,26 @@ +using System.Reactive; +using System.Security.Principal; +using PcgsInvUi.Views; +using ReactiveUI; + +namespace PcgsInvUi.ViewModels; + +public class ErrorWindowViewModel : ViewModelBase { + + public string DisplayedMessage { + get => _message; + set => this.RaiseAndSetIfChanged(ref _message, value); + } + + private string _message; + + public ReactiveCommand CloseCommand { get; } + + public ErrorWindowViewModel(string message) + { + DisplayedMessage = message; + + CloseCommand = ReactiveCommand.Create(window => window.Close()); + } + +} \ No newline at end of file diff --git a/PcgsInvUi/ViewModels/FindWindowViewModel.cs b/PcgsInvUi/ViewModels/FindWindowViewModel.cs new file mode 100644 index 0000000..6e7b70f --- /dev/null +++ b/PcgsInvUi/ViewModels/FindWindowViewModel.cs @@ -0,0 +1,8 @@ +using System.Reactive; +using ReactiveUI; + +namespace PcgsInvUi.ViewModels; + +public class FindWindowViewModel : ViewModelBase { + public ReactiveCommand OkCommand { get; } = ReactiveCommand.Create(() => (object)1); +} \ No newline at end of file diff --git a/PcgsInvUi/ViewModels/MainWindowViewModel.cs b/PcgsInvUi/ViewModels/MainWindowViewModel.cs index 292aeaf..05d3433 100644 --- a/PcgsInvUi/ViewModels/MainWindowViewModel.cs +++ b/PcgsInvUi/ViewModels/MainWindowViewModel.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive; using System.Reactive.Linq; using PcgsInvUi.Models; using PcgsInvUi.Services; @@ -6,61 +10,114 @@ using ReactiveUI; namespace PcgsInvUi.ViewModels; -public class MainWindowViewModel : ViewModelBase -{ - public CoinListViewModel List { get; } - private ViewModelBase _mainContent; - private ViewModelBase _sideContent; - - public MainWindowViewModel(CoinCollection coins) - { - List = new CoinListViewModel(coins.GetItems()); - MainContent = List; - } - - public ViewModelBase MainContent - { - get => _mainContent; - private set => this.RaiseAndSetIfChanged(ref _mainContent, value); - } +// TODO Error handling for API calls. +// TODO Manual handling of API key. +// TODO Use CoinFacts link. +// TODO Splash screen during startup. +// TODO Take giant sheet of coins to integrate into the application. +// TODO Update all coins. Choose those that are older first. +// TODO API request tracker (1000 per day) +// TODO Ok button greys out while request is happening. +// TODO Track when coins were last updated. +// TODO Image for coin in notes view. - public ViewModelBase SidebarContent - { +public class MainWindowViewModel : ViewModelBase { + public ViewModelBase SidebarContent { get => _sideContent; private set => this.RaiseAndSetIfChanged(ref _sideContent, value); } + public ObservableCollection DisplayedList { get; set; } + public CoinDatabase ConnectedCoinDatabase { get; set; } + public Coin? SelectedCoin { + get => _selectedCoin; + set => this.RaiseAndSetIfChanged(ref _selectedCoin, value); + } + public double TotalValue { + get => _totalValue; + set => this.RaiseAndSetIfChanged(ref _totalValue, value); + } - public void AddItem() - { + public ReactiveCommand DeleteCommand { get; } + public Interaction ShowDeleteWindow { get; } + public ReactiveCommand ExportCommand { get; } + public Interaction ShowExportWindow { get; } + public Interaction ShowErrorWindow { get; } + + private ViewModelBase _sideContent; + private Coin? _selectedCoin; + private double _totalValue; + + public MainWindowViewModel(CoinDatabase coins) { + ConnectedCoinDatabase = coins; var newViewModel = new NewViewModel(); + _sideContent = newViewModel; + // DisplayedList = CoinCollection = new ObservableCollection(coins.GetItems()); + DisplayedList = ConnectedCoinDatabase.Collection; + TotalValue = DisplayedList.Sum(x => x.TotalPrice); + + // Set up the ability to show the error window. + ShowErrorWindow = new Interaction(); + + // Set up the ability to open the delete window. + ShowDeleteWindow = new Interaction(); + var deleteEnabled = this.WhenAnyValue(x => x.SelectedCoin) + .Select(x => x != null); + DeleteCommand = ReactiveCommand.CreateFromTask(async () => { + var deleteViewModel = new DeleteWindowViewModel(); + var result = await ShowDeleteWindow.Handle(deleteViewModel); + if (result && SelectedCoin is not null) { + ConnectedCoinDatabase.DeleteCoin(SelectedCoin); + ConnectedCoinDatabase.Collection.Remove(SelectedCoin); // possible null deref - ignored due to button being disabled. + } + }, deleteEnabled); - Observable.Merge( - newViewModel.OkCommand, - newViewModel.CancelCommand.Select(_ => (Coin?)null)) - .Take(1) - .Subscribe(model => - { - if (model != null) - List.CoinCollection.Add(model); - - MainContent = List; + ShowExportWindow = new Interaction(); + ExportCommand = ReactiveCommand.CreateFromTask(async () => { + try { + var filePath = await ShowExportWindow.Handle(Unit.Default); + var stream = File.Open(filePath.AbsolutePath, FileMode.OpenOrCreate); + var writer = new StreamWriter(stream); + await writer.WriteAsync(ConnectedCoinDatabase.CollectionToCSV("CollectionTable")); + await writer.FlushAsync(); + } + catch (IOException e) { + // Something is wrong with the file after it's been created. + Console.WriteLine(e.Message); + ShowErrorWindow.Handle(new ErrorWindowViewModel(e.Message)); + } catch (Exception e) { + Console.WriteLine(e.Message); + ShowErrorWindow.Handle(new ErrorWindowViewModel(e.Message)); + } + }); + + // Subscribe to SidebarContent.OkCommand, which is an IObservable<(int, string, int)> + // This is the same as the above, but with a different syntax. + newViewModel.OkCommand + .Subscribe(async requestStructure => { + var result = await ConnectedCoinDatabase.CreateCoin(requestStructure.Item1, requestStructure.Item2, + requestStructure.Item3); + + switch (result) { + case PcgsClient.ErrorType.ApiKeyInvalid: + Console.WriteLine("API key is invalid - showing window."); + await ShowErrorWindow.Handle(new ErrorWindowViewModel("API key is invalid - authorization failed.")); + break; + case PcgsClient.ErrorType.InvalidRequestFormat: + Console.WriteLine("Request format is invalid - showing window."); + await ShowErrorWindow.Handle(new ErrorWindowViewModel("Request format is invalid.")); + break; + case PcgsClient.ErrorType.NoCoinFound: + Console.WriteLine("No coin was found with the given parameters."); + await ShowErrorWindow.Handle(new ErrorWindowViewModel("No coin was found with the given parameters.")); + break; + } + + // TODO Clear the text boxes. }); - - MainContent = newViewModel; - } - public void EditItem() - { - var editViewModel = new EditViewModel(List.SelectedCoin); + // Change TotalValue anytime SelectedCoin.TotalPrice changes. + // Possible null deref - how do you remove this? + this.WhenAnyValue(x => x.SelectedCoin.TotalPrice) + .Subscribe(_ => TotalValue = ConnectedCoinDatabase.Collection.Sum(x => x.TotalPrice)); } - - public void DeleteItem() - { - - } - - public void FindItem() - { - - } -} +} \ No newline at end of file diff --git a/PcgsInvUi/ViewModels/NewViewModel.cs b/PcgsInvUi/ViewModels/NewViewModel.cs index 5d4ed54..dca166d 100644 --- a/PcgsInvUi/ViewModels/NewViewModel.cs +++ b/PcgsInvUi/ViewModels/NewViewModel.cs @@ -1,36 +1,32 @@ -using System; -using System.Reactive; +using System.Reactive; using System.Collections.Generic; -using PcgsInvUi.Models; using ReactiveUI; namespace PcgsInvUi.ViewModels; -public class NewViewModel : ViewModelBase -{ - private string _pcgsNumber; - private string _grade; - private string _quantity; - - public string PcgsNumber - { +public class NewViewModel : ViewModelBase { + private string _pcgsNumber = ""; + private string _grade = ""; + private string _quantity = ""; + + public string PcgsNumber { get => _pcgsNumber; set => this.RaiseAndSetIfChanged(ref _pcgsNumber, value); } - public string Grade - { + + public string Grade { get => _grade; set => this.RaiseAndSetIfChanged(ref _grade, value); } - public string Quantity - { + + public string Quantity { get => _quantity; set => this.RaiseAndSetIfChanged(ref _quantity, value); } + public List GradesList { get; } - public NewViewModel() - { + public NewViewModel() { var okEnabled = this.WhenAnyValue( x => x.PcgsNumber, x => x.Grade, @@ -39,19 +35,19 @@ public class NewViewModel : ViewModelBase !string.IsNullOrWhiteSpace(grade) && !string.IsNullOrWhiteSpace(quantity)); - // This will eventually send our request to the public API, then construct our coin. OkCommand = ReactiveCommand.Create( - () => new Coin { PcgsNumber = Int32.Parse(PcgsNumber), - Grade = Grade, - Quantity = Int32.Parse(Quantity) }, + () => (int.Parse(PcgsNumber), Grade, int.Parse(Quantity)), okEnabled); - - CancelCommand = ReactiveCommand.Create(() => { }); + + ClearCommand = ReactiveCommand.Create(() => { + PcgsNumber = ""; + Grade = ""; + Quantity = ""; + }); // Initialize grade values for use in the view. // This must be typed out manually, as there is no way to get a list of grades from the API. - GradesList = new List() - { + GradesList = new List { "P-01", "FR-02", "AG-03", @@ -85,7 +81,6 @@ public class NewViewModel : ViewModelBase }; } - public ReactiveCommand OkCommand { get; } - public ReactiveCommand CancelCommand { get; } - + public ReactiveCommand OkCommand { get; } + public ReactiveCommand ClearCommand { get; } } \ No newline at end of file diff --git a/PcgsInvUi/ViewModels/ViewModelBase.cs b/PcgsInvUi/ViewModels/ViewModelBase.cs index 6ea13e7..03a9024 100644 --- a/PcgsInvUi/ViewModels/ViewModelBase.cs +++ b/PcgsInvUi/ViewModels/ViewModelBase.cs @@ -2,6 +2,4 @@ namespace PcgsInvUi.ViewModels; -public class ViewModelBase : ReactiveObject -{ -} +public class ViewModelBase : ReactiveObject { } \ No newline at end of file diff --git a/PcgsInvUi/Views/CoinListView.axaml b/PcgsInvUi/Views/CoinListView.axaml deleted file mode 100644 index 243a50b..0000000 --- a/PcgsInvUi/Views/CoinListView.axaml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -