diff --git a/.idea/.idea.pcgs-inv/.idea/.gitignore b/.idea/.idea.pcgs-inv/.idea/.gitignore deleted file mode 100644 index 37a38b9..0000000 --- a/.idea/.idea.pcgs-inv/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/.idea.pcgs-inv.iml -/contentModel.xml -/modules.xml -/projectSettingsUpdater.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.idea.pcgs-inv/.idea/avalonia.xml b/.idea/.idea.pcgs-inv/.idea/avalonia.xml deleted file mode 100644 index 091e888..0000000 --- a/.idea/.idea.pcgs-inv/.idea/avalonia.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/.idea.pcgs-inv/.idea/discord.xml b/.idea/.idea.pcgs-inv/.idea/discord.xml deleted file mode 100644 index d8e9561..0000000 --- a/.idea/.idea.pcgs-inv/.idea/discord.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.pcgs-inv/.idea/encodings.xml b/.idea/.idea.pcgs-inv/.idea/encodings.xml deleted file mode 100644 index df87cf9..0000000 --- a/.idea/.idea.pcgs-inv/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/.idea.pcgs-inv/.idea/indexLayout.xml b/.idea/.idea.pcgs-inv/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/.idea/.idea.pcgs-inv/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/PcgsInvUi/App.axaml.cs b/PcgsInvUi/App.axaml.cs index e71a99c..8501681 100644 --- a/PcgsInvUi/App.axaml.cs +++ b/PcgsInvUi/App.axaml.cs @@ -1,12 +1,9 @@ -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; @@ -23,14 +20,11 @@ public partial class App : Application { DataContext = new MainWindowViewModel(coins), }; + // Allow the application to save the database when it exits. 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/Services/CoinCollection.cs b/PcgsInvUi/Services/CoinCollection.cs deleted file mode 100644 index dc89845..0000000 --- a/PcgsInvUi/Services/CoinCollection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using PcgsInvUi.Models; - -namespace PcgsInvUi.Services; - -public class CoinCollection { - public IEnumerable GetItems() => new[] { - new Coin(), - new Coin(), - new Coin(), - }; -} \ No newline at end of file diff --git a/PcgsInvUi/Services/CoinDatabase.cs b/PcgsInvUi/Services/CoinDatabase.cs index 221d845..03e6cc1 100644 --- a/PcgsInvUi/Services/CoinDatabase.cs +++ b/PcgsInvUi/Services/CoinDatabase.cs @@ -10,23 +10,29 @@ public class CoinDatabase { public SQLiteConnection Connection { get; set; } private Boolean IsConnected { get; set; } public ObservableCollection Collection { get; set; } + public Boolean IsClientConnected { get; set; } - private PcgsClient _pcgsClient; - + private PcgsClient? _pcgsClient; + + /// + /// Creates and initilizes a new instance of the CoinDatabase. + /// Must call TryInitApiClient() after creating a new instance. + /// 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"); + CreateApiTable(); Console.WriteLine("Connected to database."); Collection = new ObservableCollection(); GetCollection("CollectionTable"); Console.WriteLine("Got collection."); } + /// + /// Destructor. Currently not used, due to the strange nature of desktop applications. + /// ~CoinDatabase() { Console.WriteLine("Destructor called, saving collection."); // Save collection. @@ -34,13 +40,74 @@ public class CoinDatabase { // Close the connection. Connection.Close(); } + + /// + /// Attempts to intitialize the API client by selecting the API key from the database. + /// + /// True if the API key was found, false otherwise. + public async Task TryInitApiClient() { + Console.Write("Attempting to initialize API client... "); + SQLiteCommand cmd = Connection.CreateCommand(); + cmd.CommandText = "SELECT API_KEY FROM ApiTable"; + using (var reader = cmd.ExecuteReader()) { + if (reader.Read()) { + var apiKey = reader.GetString(0); + _pcgsClient = await PcgsClient.CreateClient(apiKey); + + // Check that the client is valid. + if (_pcgsClient is null) { + IsClientConnected = false; + Console.WriteLine("Failed. API key was invalid."); + return IsClientConnected; + } + + IsClientConnected = true; + Console.WriteLine("Done! API key was found from database."); + } + else { + IsClientConnected = false; + Console.WriteLine("Failed. API key not found."); + } + } + return IsClientConnected; + } + + /// + /// Atttempts to initialize the PCGS API client. + /// + /// The API key to verify. + /// A Task with a return value that returns true if the client intializes properly. + public async Task TryInitApiClient(string apiKey) { + Console.Write("Attempting to initialize API client with key param... "); + _pcgsClient = await PcgsClient.CreateClient(apiKey); + // Check if the API key is valid and if it is, then add to the database. + if (_pcgsClient is null) { + IsClientConnected = false; + Console.WriteLine("Failed. API key was invalid."); + return IsClientConnected; + } + + var cmd = Connection.CreateCommand(); + cmd.CommandText = $"INSERT INTO ApiTable (API_KEY, REQUESTS_REMAINING) VALUES (\"{apiKey}\", 1000)"; + await cmd.ExecuteNonQueryAsync(); + + IsClientConnected = true; + Console.WriteLine("Done! API key was added to database."); + return IsClientConnected; + } + + /// + /// Gets the collection from the database and stores it in the Collection property. + /// Creates the collection table if it does not exist already. + /// + /// The SQLiteConnection that corresponds to the connected database. private SQLiteConnection CreateConnection() { // Check if database exists. if (!System.IO.File.Exists("coin_collection")) { - // Create the database. + // If not, create the database. Console.Write("Creating database... "); - SQLiteConnection.CreateFile("coin_collection"); + SQLiteConnection.CreateFile("coin_collection.sqlite"); Console.WriteLine("Done."); } @@ -60,6 +127,33 @@ public class CoinDatabase { return connection; } + /// + /// Creates the table that stores the API key and the number of requests remaining. + /// + public void CreateApiTable() { + // Create a table to store the API key and the number of requests remaining. + 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 ApiTable (" + + "API_KEY TEXT, " + + "REQUESTS_REMAINING INTEGER, " + + "PRIMARY KEY(API_KEY))"; + + // Execute the command + cmd.ExecuteNonQuery(); + Console.WriteLine("API table initialized."); + } + + /// + /// Creates a table to store the collection. + /// public void CreateCollection(string collectionName) { // Create a table to store the collection. if (!IsConnected) { @@ -93,8 +187,12 @@ public class CoinDatabase { // Execute the command. cmd.ExecuteNonQuery(); + Console.WriteLine("Collection table initialized."); } + /// + /// Inserts a Coin object into the database. + /// public void InsertCoin(Coin coin) { Console.WriteLine("Inserting coin..."); // Use our existing connection to insert a coin to the database. @@ -121,6 +219,13 @@ public class CoinDatabase { Console.WriteLine("Coin inserted."); } + /// + /// Creates a Coin object and saves it to the database. + /// + /// The PCGS number of the coin. + /// The grade of the coin. + /// The quantity of the coin. + /// The error type returned by the API. 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. @@ -146,6 +251,12 @@ public class CoinDatabase { return apiResult.Item1; } + /// + /// Gets a coin from the database. + /// + /// The PCGS number of the coin. + /// The grade of the coin. + /// The coin if it exists, null otherwise. public Coin? GetCoin(int pcgsNumber, string grade) { Console.WriteLine($"Getting coin {pcgsNumber}, {grade}..."); int gradeInt = GetGradeFromString(grade); @@ -165,6 +276,10 @@ public class CoinDatabase { return null; } + /// + /// Used to get all Coin objects within the database. + /// + /// The name of the collection table. public void GetCollection(string collectionName) { Console.WriteLine("Getting collection..."); SQLiteCommand cmd = Connection.CreateCommand(); @@ -178,6 +293,11 @@ public class CoinDatabase { } } + /// + /// Creates a CSV formatted string from a collection. + /// + /// The name of the collection table. + /// A CSV formatted string. public String CollectionToCSV(string collectionName) { var cmd = Connection.CreateCommand(); cmd.CommandText = $"SELECT * FROM {collectionName}"; @@ -226,6 +346,11 @@ public class CoinDatabase { return resultString; } + /// + /// Saves a Coin object to the database. + /// + /// The name of the collection table. + /// The coin to save. public void SaveCoin(string collectionName, Coin coin) { // Get and replace the coin from the database. Console.WriteLine("Saving coin..."); @@ -252,6 +377,10 @@ public class CoinDatabase { cmd.ExecuteNonQuery(); } + /// + /// Synchronizes the database with the current contents of the specified collection. + /// + /// The name of the collection table. 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. @@ -260,6 +389,10 @@ public class CoinDatabase { } } + /// + /// Creates a Coin from the information in a reader specifically from this database. + /// + /// The reader to get the information from. private Coin CoinFromReader(SQLiteDataReader reader) { // Create a coin while filtering out any possible null values - they throw exceptions. Coin coin = new Coin(); @@ -283,6 +416,10 @@ public class CoinDatabase { return coin; } + /// + /// Deletes a Coin object from the database. + /// + /// The coin to delete. public void DeleteCoin(Coin delCoin) { SQLiteCommand cmd = Connection.CreateCommand(); cmd.CommandText = "DELETE FROM CollectionTable WHERE PCGS_NUMBER = @PcgsNumber AND GRADE = @Grade"; @@ -292,9 +429,18 @@ public class CoinDatabase { cmd.ExecuteNonQuery(); } + /// + /// Gets the grade from a string returned from the main window. + /// These are always in a format similar to "MS-65", so we can just get the last two characters. + /// + /// The grade string. 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 +} + +public class NotConnectedToDatabaseException : Exception { + public NotConnectedToDatabaseException() : base("Not connected to database.") { } +} diff --git a/PcgsInvUi/Services/PcgsClient.cs b/PcgsInvUi/Services/PcgsClient.cs index e4329d2..0f70915 100644 --- a/PcgsInvUi/Services/PcgsClient.cs +++ b/PcgsInvUi/Services/PcgsClient.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using PcgsInvUi.Models; using System.Text.Json; -using DynamicData.Kernel; using ReactiveUI; namespace PcgsInvUi.Services; @@ -27,14 +25,47 @@ public class PcgsClient : ReactiveObject { private readonly string _apiKey; private int _numApiRequests; - public PcgsClient(string apiKey) { - // _apiKey = apiKey; - _apiKey = - "E8xivTVN_k4Q8ubk8Xv_uU11U91WX0wDht4S4m4BAf2HPmKnGrZ0J2f9nccM7Ns1VkGoT1SXYR74I7aNaMKqXlJGEy3oobseKV0nGiqu1IKvFkOTVjhV37SWUVExSDFMC6FLzs7xJQIHwkcxr5JFOQNmc4ZytEh_-hmCN3NL-z0fEk98tzBmPJ_F7bq_xNru86QMD2A9yalu_dWe-lCysfyE2bCA2EDqwvWenRYf-0XnNtp6qlxxCMlCd-TDqqRKLC3qefCwpXSemwBiNSP5gbC9pwcKrZugkruuJ0nyxxThZymm"; + /// + /// Creates a new instance of the PcgsClient class. + /// Although this is a constructor, it's meant to be called only from the CreateClient method. + /// + private PcgsClient(string apiKey) { + _apiKey = apiKey; + } + + /// + /// Creates a new instance of the PcgsClient class. + /// + /// The api key to use for requests. + /// A new instance of the PcgsClient class, or null if the key is invalid. + public static async Task CreateClient(string apiKey) { + // Check if the api key is valid. + var testClient = new PcgsClient(apiKey); + var validCreds = await testClient.VerifyCredentials(); + if (!validCreds) return null; + return testClient; } + /// + /// Sends a request to the API to verify the credentials. + /// + /// A boolean indicating whether or not the credentials are valid. + public async Task VerifyCredentials() { + // Create a way to check if the api key given is valid + // I think the best way to do this is to make a request to the api and see if it returns the expected response. + var result = await this.GetCoinFactsByGrade(1, "AG-03"); // This is a known invalid coin id - this is because the api is very fast with invalid requests. + if (result.Item1 == ErrorType.ApiKeyInvalid) return false; + return true; + } + + /// + /// Gets the coin information for a given PCGS number and grade. + /// + /// The PCGS number of the coin. + /// The grade of the coin. + /// Whether or not the coin is a plus grade. + /// A tuple containing the error type and the coin information. public async Task<(ErrorType, Coin?)> GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) { - Coin newCoin = null; using (HttpClient client = new HttpClient()) { var gradeParam = GetGradeFromString(grade); @@ -46,6 +77,7 @@ public class PcgsClient : ReactiveObject { Console.WriteLine("Got response."); if (response.IsSuccessStatusCode) { + // Check response for errors. string jsonResponse = await response.Content.ReadAsStringAsync(); var parsedResponse = ParseJsonResponse(jsonResponse); return parsedResponse; @@ -55,17 +87,25 @@ public class PcgsClient : ReactiveObject { return (ErrorType.ApiKeyInvalid, null); } + /// + /// Utility function to parse the grade number from the grade string passed from the front end. + /// + /// The grade string. + /// The grade number. private int GetGradeFromString(string grade) { - var actualGrade = int.Parse(grade.Substring(grade.Length - 2)); + var actualGrade = int.Parse(grade.Substring(grade.Length - 2)); // Throws an exception if the grade doesn't exist. Console.WriteLine($"Grade is {actualGrade}"); return actualGrade; } - // TODO Will eventually create the other methods that are included in the Public API. - + /// + /// Parses the json response from the api. + /// + /// The json response. + /// A tuple containing an error type and the coin information. private (ErrorType, Coin?) ParseJsonResponse(string json) { Console.WriteLine(json); - // TODO Check for errors in the response + // 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); @@ -86,4 +126,4 @@ 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/ViewModels/ApiKeyViewModel.cs b/PcgsInvUi/ViewModels/ApiKeyViewModel.cs new file mode 100644 index 0000000..468b2dd --- /dev/null +++ b/PcgsInvUi/ViewModels/ApiKeyViewModel.cs @@ -0,0 +1,30 @@ +using ReactiveUI; +using System.Reactive; + +namespace PcgsInvUi.ViewModels; + +public class ApiKeyViewModel : ViewModelBase { + public string ApiKeyInput { + get => _apiKeyInput; + set => this.RaiseAndSetIfChanged(ref _apiKeyInput, value); + } + private string _apiKeyInput = ""; + + public bool OkEnabled { + get => _okEnabled; + set => this.RaiseAndSetIfChanged(ref _okEnabled, value); + } + private bool _okEnabled; + + public ReactiveCommand OkCommand { get; } + + public ApiKeyViewModel() { + var okEnabled = this.WhenAnyValue( + x => x.ApiKeyInput, + x => !string.IsNullOrWhiteSpace(x)); + + + // Get the API key from the user. + OkCommand = ReactiveCommand.Create(() => ApiKeyInput, okEnabled); + } +} diff --git a/PcgsInvUi/ViewModels/MainWindowViewModel.cs b/PcgsInvUi/ViewModels/MainWindowViewModel.cs index af680ba..2291d54 100644 --- a/PcgsInvUi/ViewModels/MainWindowViewModel.cs +++ b/PcgsInvUi/ViewModels/MainWindowViewModel.cs @@ -7,21 +7,10 @@ using System.Reactive.Linq; using PcgsInvUi.Models; using PcgsInvUi.Services; using ReactiveUI; +using System.Threading.Tasks; namespace PcgsInvUi.ViewModels; -// TODO Create proper sums for total 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 class MainWindowViewModel : ViewModelBase { public ViewModelBase SidebarContent { get => _sideContent; @@ -47,14 +36,29 @@ public class MainWindowViewModel : ViewModelBase { private ViewModelBase _sideContent; private Coin? _selectedCoin; private double _totalValue; - + public MainWindowViewModel(CoinDatabase coins) { ConnectedCoinDatabase = coins; + var keyFound = Task.Run(() => coins.TryInitApiClient()).Result; + Console.WriteLine("Function completed."); var newViewModel = new NewViewModel(); - _sideContent = newViewModel; + var apiKeyViewModel = new ApiKeyViewModel(); // DisplayedList = CoinCollection = new ObservableCollection(coins.GetItems()); DisplayedList = ConnectedCoinDatabase.Collection; TotalValue = DisplayedList.Sum(x => x.TotalPrice); + + // Setup interaction to show ApiKeyWindow + apiKeyViewModel.OkCommand.Subscribe(async apiKey => { + if (await ConnectedCoinDatabase.TryInitApiClient(apiKey)) + SidebarContent = newViewModel; // Must assign this to the observable property rather than private variable. + }); + + // Check if API key must be entered. + if (!keyFound) { + Console.WriteLine("API key must be entered by user."); + // Show the API key input view. + _sideContent = apiKeyViewModel; + } else _sideContent = newViewModel; // Set up the ability to show the error window. ShowErrorWindow = new Interaction(); @@ -113,7 +117,8 @@ public class MainWindowViewModel : ViewModelBase { break; } - // TODO Clear the text boxes. + // Clear the text boxes. + newViewModel.ClearText(); }); // Update TotalValue anytime a coin's quantity changes. @@ -126,4 +131,11 @@ public class MainWindowViewModel : ViewModelBase { TotalValue = ConnectedCoinDatabase.Collection.Sum(x => x.TotalPrice); }; } + + // Create destructor to save the database when the application exits. + // This is necessary because the database is initialized in this class. + ~MainWindowViewModel() { + Console.WriteLine("Running MainWindowViewModel destructor."); + ConnectedCoinDatabase.UpdateCollection("CollectionTable"); + } } diff --git a/PcgsInvUi/ViewModels/NewViewModel.cs b/PcgsInvUi/ViewModels/NewViewModel.cs index dca166d..3909324 100644 --- a/PcgsInvUi/ViewModels/NewViewModel.cs +++ b/PcgsInvUi/ViewModels/NewViewModel.cs @@ -40,9 +40,7 @@ public class NewViewModel : ViewModelBase { okEnabled); ClearCommand = ReactiveCommand.Create(() => { - PcgsNumber = ""; - Grade = ""; - Quantity = ""; + ClearText(); }); // Initialize grade values for use in the view. @@ -83,4 +81,13 @@ public class NewViewModel : ViewModelBase { public ReactiveCommand OkCommand { get; } public ReactiveCommand ClearCommand { get; } -} \ No newline at end of file + + /// + /// Clears the text fields. + /// + public void ClearText() { + PcgsNumber = ""; + Grade = ""; + Quantity = ""; + } +} diff --git a/PcgsInvUi/Views/ApiKeyView.axaml b/PcgsInvUi/Views/ApiKeyView.axaml new file mode 100644 index 0000000..e125862 --- /dev/null +++ b/PcgsInvUi/Views/ApiKeyView.axaml @@ -0,0 +1,14 @@ + + +