diff --git a/PcgsInvUi/App.axaml.cs b/PcgsInvUi/App.axaml.cs index ed78335..e71a99c 100644 --- a/PcgsInvUi/App.axaml.cs +++ b/PcgsInvUi/App.axaml.cs @@ -1,3 +1,5 @@ +using System; +using System.Data.Entity.Migrations.Model; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -15,15 +17,20 @@ public partial class App : Application { public override void OnFrameworkInitializationCompleted() { base.OnFrameworkInitializationCompleted(); - 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 ee6ebe6..2d9e8bf 100644 --- a/PcgsInvUi/Models/Coin.cs +++ b/PcgsInvUi/Models/Coin.cs @@ -1,5 +1,4 @@ -using System; -using ReactiveUI; +using ReactiveUI; namespace PcgsInvUi.Models; diff --git a/PcgsInvUi/PcgsInvUi.csproj b/PcgsInvUi/PcgsInvUi.csproj index a1ce9f5..8a56561 100644 --- a/PcgsInvUi/PcgsInvUi.csproj +++ b/PcgsInvUi/PcgsInvUi.csproj @@ -1,7 +1,7 @@  WinExe - net6.0 + net7.0 enable true app.manifest @@ -15,13 +15,13 @@ - - - - + + + + - - + + @@ -83,4 +83,9 @@ + + + + + diff --git a/PcgsInvUi/Services/CoinDatabase.cs b/PcgsInvUi/Services/CoinDatabase.cs index cdee8f5..a5ec1ac 100644 --- a/PcgsInvUi/Services/CoinDatabase.cs +++ b/PcgsInvUi/Services/CoinDatabase.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Data.SQLite; using PcgsInvUi.Models; @@ -8,13 +8,30 @@ 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() { @@ -62,13 +79,15 @@ public class CoinDatabase { "DESIGNATION TEXT," + "CERT_NUM INTEGER," + "QUANTITY INTEGER," + + "NOTES TEXT," + "PRIMARY KEY(PCGS_NUMBER, GRADE))"; // Execute the command. cmd.ExecuteNonQuery(); } - public void InsertCoin(Coin newCoin) { + 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 @@ -82,44 +101,63 @@ public class CoinDatabase { "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) " + - "VALUES (@PcgsNumber, @Grade, @Year, @Denomination, @MintMark, @PriceGuideValue," + - "@CoinFactsLink, @MajorVariety, @MinorVariety, @DieVariety, @SeriesName, @Category, @Designation," + - "@CertificateNumber, @Quantity)"; - cmd.Parameters.AddWithValue("@PcgsNumber", newCoin.PCGSNo); - cmd.Parameters.AddWithValue("@Grade", newCoin.Grade); - cmd.Parameters.AddWithValue("@Year", newCoin.Year); - cmd.Parameters.AddWithValue("@Denomination", newCoin.Denomination); - cmd.Parameters.AddWithValue("@MintMark", newCoin.MintMark); - cmd.Parameters.AddWithValue("@PriceGuideValue", newCoin.PriceGuideValue); - cmd.Parameters.AddWithValue("@CoinFactsLink", newCoin.CoinFactsLink); - cmd.Parameters.AddWithValue("@MajorVariety", newCoin.MajorVariety); - cmd.Parameters.AddWithValue("@MinorVariety", newCoin.MinorVariety); - cmd.Parameters.AddWithValue("@DieVariety", newCoin.DieVariety); - cmd.Parameters.AddWithValue("@SeriesName", newCoin.SeriesName); - cmd.Parameters.AddWithValue("@Category", newCoin.Category); - cmd.Parameters.AddWithValue("@Designation", newCoin.Designation); - cmd.Parameters.AddWithValue("@CertificateNumber", newCoin.CertificateNumber); - cmd.Parameters.AddWithValue("@Quantity", newCoin.Quantity); + "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(); } - public List GetCollection(string collectionName) { + public async void 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); + } + else { + // Build a new coin with the API and insert it. + var newCoin = await _pcgsClient.GetCoinFactsByGrade(pcgsNumber, grade); + newCoin.Quantity = quantity; + InsertCoin(newCoin); + } + } + + 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}"; - - var coins = new List(); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { // Create coin from reader then add to list. - coins.Add(CoinFromReader(reader)); + Collection.Add(CoinFromReader(reader)); } } - - return coins; } public String CollectionToCSV(string collectionName) { @@ -163,54 +201,70 @@ public class CoinDatabase { resultString += "NULL" + ","; } } - resultString += "\n"; } } return resultString; } - - public Coin? GetCoin(int pcgsNumber, string grade) { - // Get the coin based off of the pcgs number. + + 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 = "SELECT * FROM CollectionTable WHERE Id = @Id AND Grade = @Grade"; - cmd.Parameters.AddWithValue("@Id", pcgsNumber); - cmd.Parameters.AddWithValue("@Grade", grade); - - Coin? returnCoin = null; - + 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. - using (var reader = cmd.ExecuteReader()) { - while (reader.Read()) { - returnCoin = CoinFromReader(reader); - } - } + cmd.ExecuteNonQuery(); + } - return returnCoin; + 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(); - coin.PCGSNo = reader.GetString(0); - coin.Grade = reader.GetString(1); - coin.Year = reader.GetInt32(2); - coin.Denomination = reader.GetString(3); - coin.MintMark = reader.GetString(4); - coin.PriceGuideValue = reader.GetDouble(5); - coin.CoinFactsLink = reader.GetString(6); - coin.MajorVariety = reader.GetString(7); - coin.MinorVariety = reader.GetString(8); - coin.DieVariety = reader.GetString(9); - coin.SeriesName = reader.GetString(10); - coin.Category = reader.GetString(11); - coin.Designation = reader.GetString(12); - coin.CertificateNumber = reader.GetInt32(13); - coin.Quantity = reader.GetInt32(14); + 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"; @@ -219,4 +273,10 @@ public class CoinDatabase { 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/PcgsClient.cs b/PcgsInvUi/Services/PcgsClient.cs index 791f3fe..3b39b49 100644 --- a/PcgsInvUi/Services/PcgsClient.cs +++ b/PcgsInvUi/Services/PcgsClient.cs @@ -2,16 +2,22 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -// using Newtonsoft.Json; using PcgsInvUi.Models; using System.Text.Json; -using System.Text.Json.Serialization; +using ReactiveUI; + namespace PcgsInvUi.Services; -public class PcgsClient { +public class PcgsClient : ReactiveObject { + 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; diff --git a/PcgsInvUi/ViewModels/MainWindowViewModel.cs b/PcgsInvUi/ViewModels/MainWindowViewModel.cs index ad046d2..4bc71af 100644 --- a/PcgsInvUi/ViewModels/MainWindowViewModel.cs +++ b/PcgsInvUi/ViewModels/MainWindowViewModel.cs @@ -10,7 +10,7 @@ using ReactiveUI; namespace PcgsInvUi.ViewModels; -// TODO Error handling. +// TODO Error handling for API calls. // TODO Manual handling of API key. // TODO Use CoinFacts link. // TODO Splash screen during startup. @@ -26,7 +26,6 @@ public class MainWindowViewModel : ViewModelBase { get => _sideContent; private set => this.RaiseAndSetIfChanged(ref _sideContent, value); } - public ObservableCollection CoinCollection { get; set; } public ObservableCollection DisplayedList { get; set; } public CoinDatabase ConnectedCoinDatabase { get; set; } public Coin? SelectedCoin { @@ -53,12 +52,9 @@ public class MainWindowViewModel : ViewModelBase { var newViewModel = new NewViewModel(); _sideContent = newViewModel; // DisplayedList = CoinCollection = new ObservableCollection(coins.GetItems()); - DisplayedList = CoinCollection = - new ObservableCollection(coins.GetCollection("CollectionTable")); + DisplayedList = ConnectedCoinDatabase.Collection; TotalValue = DisplayedList.Sum(x => x.TotalPrice); - _pcgsClient = new PcgsClient( - "eAb8gS0I2XAvT_5gJeiGJaglMia1Tk-oB4kJUK6kuafyrny_S61vIJY-Ikl4nCQM67wrdxzUqLVWTV2kBSxD3d5XNBHxHnYBhcSS6dOPug0hZaF3qAv56df3gYSzOGh9Tif5y0eP3Iw0LrqKDr1Hj-dk6SV6GKog2IIqCPQhhHH8FMTWBTYO-_O8cx7qLdM5GM8KlTsic6g3VRUhM8EA_4OO04dCfmNLGhqINRl3jGZ0Q4ziI8fng2bVWsIyteqiPzUn10rIQ3-OPpqVZG_DxeOmOejj4GzbUNyqUOajy-nr5rYY"); - + // Set up the ability to open the delete window. ShowDeleteWindow = new Interaction(); var deleteEnabled = this.WhenAnyValue(x => x.SelectedCoin) @@ -68,7 +64,7 @@ public class MainWindowViewModel : ViewModelBase { var result = await ShowDeleteWindow.Handle(deleteViewModel); if (result && SelectedCoin is not null) { ConnectedCoinDatabase.DeleteCoin(SelectedCoin); - CoinCollection.Remove(SelectedCoin); // possible null deref - ignored due to button being disabled. + ConnectedCoinDatabase.Collection.Remove(SelectedCoin); // possible null deref - ignored due to button being disabled. } }, deleteEnabled); @@ -93,16 +89,12 @@ public class MainWindowViewModel : ViewModelBase { // This is the same as the above, but with a different syntax. newViewModel.OkCommand .Subscribe(async requestStructure => { - var newCoin = await _pcgsClient.GetCoinFactsByGrade(requestStructure.Item1, requestStructure.Item2); - newCoin.Quantity = requestStructure.Item3; - CoinCollection.Add(newCoin); - coins.InsertCoin(newCoin); - TotalValue += newCoin.Quantity * newCoin.PriceGuideValue; + ConnectedCoinDatabase.CreateCoin(requestStructure.Item1, requestStructure.Item2, requestStructure.Item3); }); // Change TotalValue anytime SelectedCoin.TotalPrice changes. // Possible null deref - ignored due to null values summing to 0. this.WhenAnyValue(x => x.SelectedCoin.TotalPrice) - .Subscribe(_ => TotalValue = CoinCollection.Sum(x => x.TotalPrice)); + .Subscribe(_ => TotalValue = ConnectedCoinDatabase.Collection.Sum(x => x.TotalPrice)); } } \ No newline at end of file diff --git a/PcgsInvUi/Views/MainWindow.axaml b/PcgsInvUi/Views/MainWindow.axaml index fafc7dd..a351c12 100644 --- a/PcgsInvUi/Views/MainWindow.axaml +++ b/PcgsInvUi/Views/MainWindow.axaml @@ -14,7 +14,9 @@ - +