Merge pull request from BobdaFett/api-key-management
Add API key management.
This commit was merged in pull request #10.
This commit is contained in:
14 files changed
+312
-58
No files matched your search
@@ -1,12 +1,9 @@
|
|||||||
using System;
|
|
||||||
using System.Data.Entity.Migrations.Model;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using PcgsInvUi.Services;
|
using PcgsInvUi.Services;
|
||||||
using PcgsInvUi.ViewModels;
|
using PcgsInvUi.ViewModels;
|
||||||
using PcgsInvUi.Views;
|
using PcgsInvUi.Views;
|
||||||
using PcgsInvUi.Models;
|
|
||||||
|
|
||||||
namespace PcgsInvUi;
|
namespace PcgsInvUi;
|
||||||
|
|
||||||
@@ -23,14 +20,11 @@ public partial class App : Application {
|
|||||||
DataContext = new MainWindowViewModel(coins),
|
DataContext = new MainWindowViewModel(coins),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Allow the application to save the database when it exits.
|
||||||
desktop.Exit += (sender, args) => {
|
desktop.Exit += (sender, args) => {
|
||||||
coins.UpdateCollection("CollectionTable");
|
coins.UpdateCollection("CollectionTable");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnExit(object sender, ControlledApplicationLifetimeExitEventArgs e) {
|
|
||||||
// Save the database?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using PcgsInvUi.Models;
|
|
||||||
|
|
||||||
namespace PcgsInvUi.Services;
|
|
||||||
|
|
||||||
public class CoinCollection {
|
|
||||||
public IEnumerable<Coin> GetItems() => new[] {
|
|
||||||
new Coin(),
|
|
||||||
new Coin(),
|
|
||||||
new Coin(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -10,23 +10,29 @@ public class CoinDatabase {
|
|||||||
public SQLiteConnection Connection { get; set; }
|
public SQLiteConnection Connection { get; set; }
|
||||||
private Boolean IsConnected { get; set; }
|
private Boolean IsConnected { get; set; }
|
||||||
public ObservableCollection<Coin> Collection { get; set; }
|
public ObservableCollection<Coin> Collection { get; set; }
|
||||||
|
public Boolean IsClientConnected { get; set; }
|
||||||
|
|
||||||
private PcgsClient _pcgsClient;
|
private PcgsClient? _pcgsClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and initilizes a new instance of the CoinDatabase.
|
||||||
|
/// Must call TryInitApiClient() after creating a new instance.
|
||||||
|
/// </summary>
|
||||||
public CoinDatabase() {
|
public CoinDatabase() {
|
||||||
// Create a connection to SQLite
|
// Create a connection to SQLite
|
||||||
Connection = CreateConnection();
|
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.
|
// Create a base table.
|
||||||
CreateCollection("CollectionTable");
|
CreateCollection("CollectionTable");
|
||||||
|
CreateApiTable();
|
||||||
Console.WriteLine("Connected to database.");
|
Console.WriteLine("Connected to database.");
|
||||||
Collection = new ObservableCollection<Coin>();
|
Collection = new ObservableCollection<Coin>();
|
||||||
GetCollection("CollectionTable");
|
GetCollection("CollectionTable");
|
||||||
Console.WriteLine("Got collection.");
|
Console.WriteLine("Got collection.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destructor. Currently not used, due to the strange nature of desktop applications.
|
||||||
|
/// </summary>
|
||||||
~CoinDatabase() {
|
~CoinDatabase() {
|
||||||
Console.WriteLine("Destructor called, saving collection.");
|
Console.WriteLine("Destructor called, saving collection.");
|
||||||
// Save collection.
|
// Save collection.
|
||||||
@@ -35,12 +41,73 @@ public class CoinDatabase {
|
|||||||
Connection.Close();
|
Connection.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to intitialize the API client by selecting the API key from the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if the API key was found, false otherwise.</returns>
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atttempts to initialize the PCGS API client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name=apiKey>The API key to verify.</param>
|
||||||
|
/// <returns>A Task with a return value that returns true if the client intializes properly.</returns>
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection from the database and stores it in the Collection property.
|
||||||
|
/// Creates the collection table if it does not exist already.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The SQLiteConnection that corresponds to the connected database.</returns>
|
||||||
private SQLiteConnection CreateConnection() {
|
private SQLiteConnection CreateConnection() {
|
||||||
// Check if database exists.
|
// Check if database exists.
|
||||||
if (!System.IO.File.Exists("coin_collection")) {
|
if (!System.IO.File.Exists("coin_collection")) {
|
||||||
// Create the database.
|
// If not, create the database.
|
||||||
Console.Write("Creating database... ");
|
Console.Write("Creating database... ");
|
||||||
SQLiteConnection.CreateFile("coin_collection");
|
SQLiteConnection.CreateFile("coin_collection.sqlite");
|
||||||
Console.WriteLine("Done.");
|
Console.WriteLine("Done.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +127,33 @@ public class CoinDatabase {
|
|||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the table that stores the API key and the number of requests remaining.
|
||||||
|
/// </summary>
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a table to store the collection.
|
||||||
|
/// </summary>
|
||||||
public void CreateCollection(string collectionName) {
|
public void CreateCollection(string collectionName) {
|
||||||
// Create a table to store the collection.
|
// Create a table to store the collection.
|
||||||
if (!IsConnected) {
|
if (!IsConnected) {
|
||||||
@@ -93,8 +187,12 @@ public class CoinDatabase {
|
|||||||
|
|
||||||
// Execute the command.
|
// Execute the command.
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine("Collection table initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inserts a Coin object into the database.
|
||||||
|
/// </summary>
|
||||||
public void InsertCoin(Coin coin) {
|
public void InsertCoin(Coin coin) {
|
||||||
Console.WriteLine("Inserting coin...");
|
Console.WriteLine("Inserting coin...");
|
||||||
// Use our existing connection to insert a coin to the database.
|
// Use our existing connection to insert a coin to the database.
|
||||||
@@ -121,6 +219,13 @@ public class CoinDatabase {
|
|||||||
Console.WriteLine("Coin inserted.");
|
Console.WriteLine("Coin inserted.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Coin object and saves it to the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pcgsNumber">The PCGS number of the coin.</param>
|
||||||
|
/// <param name="grade">The grade of the coin.</param>
|
||||||
|
/// <param name="quantity">The quantity of the coin.</param>
|
||||||
|
/// <returns>The error type returned by the API.</returns>
|
||||||
public async Task<PcgsClient.ErrorType> CreateCoin(int pcgsNumber, string grade, int quantity) {
|
public async Task<PcgsClient.ErrorType> CreateCoin(int pcgsNumber, string grade, int quantity) {
|
||||||
Console.WriteLine("Creating coin...");
|
Console.WriteLine("Creating coin...");
|
||||||
// Create a coin by calling the API, then insert it into the database.
|
// Create a coin by calling the API, then insert it into the database.
|
||||||
@@ -146,6 +251,12 @@ public class CoinDatabase {
|
|||||||
return apiResult.Item1;
|
return apiResult.Item1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a coin from the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pcgsNumber">The PCGS number of the coin.</param>
|
||||||
|
/// <param name="grade">The grade of the coin.</param>
|
||||||
|
/// <returns>The coin if it exists, null otherwise.</returns>
|
||||||
public Coin? GetCoin(int pcgsNumber, string grade) {
|
public Coin? GetCoin(int pcgsNumber, string grade) {
|
||||||
Console.WriteLine($"Getting coin {pcgsNumber}, {grade}...");
|
Console.WriteLine($"Getting coin {pcgsNumber}, {grade}...");
|
||||||
int gradeInt = GetGradeFromString(grade);
|
int gradeInt = GetGradeFromString(grade);
|
||||||
@@ -165,6 +276,10 @@ public class CoinDatabase {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to get all Coin objects within the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collectionName">The name of the collection table.</param>
|
||||||
public void GetCollection(string collectionName) {
|
public void GetCollection(string collectionName) {
|
||||||
Console.WriteLine("Getting collection...");
|
Console.WriteLine("Getting collection...");
|
||||||
SQLiteCommand cmd = Connection.CreateCommand();
|
SQLiteCommand cmd = Connection.CreateCommand();
|
||||||
@@ -178,6 +293,11 @@ public class CoinDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a CSV formatted string from a collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collectionName">The name of the collection table.</param>
|
||||||
|
/// <returns>A CSV formatted string.</returns>
|
||||||
public String CollectionToCSV(string collectionName) {
|
public String CollectionToCSV(string collectionName) {
|
||||||
var cmd = Connection.CreateCommand();
|
var cmd = Connection.CreateCommand();
|
||||||
cmd.CommandText = $"SELECT * FROM {collectionName}";
|
cmd.CommandText = $"SELECT * FROM {collectionName}";
|
||||||
@@ -226,6 +346,11 @@ public class CoinDatabase {
|
|||||||
return resultString;
|
return resultString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves a Coin object to the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collectionName">The name of the collection table.</param>
|
||||||
|
/// <param name="coin">The coin to save.</param>
|
||||||
public void SaveCoin(string collectionName, Coin coin) {
|
public void SaveCoin(string collectionName, Coin coin) {
|
||||||
// Get and replace the coin from the database.
|
// Get and replace the coin from the database.
|
||||||
Console.WriteLine("Saving coin...");
|
Console.WriteLine("Saving coin...");
|
||||||
@@ -252,6 +377,10 @@ public class CoinDatabase {
|
|||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Synchronizes the database with the current contents of the specified collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collectionName">The name of the collection table.</param>
|
||||||
public void UpdateCollection(string collectionName) {
|
public void UpdateCollection(string collectionName) {
|
||||||
// Update the database with the contents of the collection.
|
// 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.
|
// 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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Coin from the information in a reader specifically from this database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The reader to get the information from.</param>
|
||||||
private Coin CoinFromReader(SQLiteDataReader reader) {
|
private Coin CoinFromReader(SQLiteDataReader reader) {
|
||||||
// Create a coin while filtering out any possible null values - they throw exceptions.
|
// Create a coin while filtering out any possible null values - they throw exceptions.
|
||||||
Coin coin = new Coin();
|
Coin coin = new Coin();
|
||||||
@@ -283,6 +416,10 @@ public class CoinDatabase {
|
|||||||
return coin;
|
return coin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a Coin object from the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delCoin">The coin to delete.</param>
|
||||||
public void DeleteCoin(Coin delCoin) {
|
public void DeleteCoin(Coin delCoin) {
|
||||||
SQLiteCommand cmd = Connection.CreateCommand();
|
SQLiteCommand cmd = Connection.CreateCommand();
|
||||||
cmd.CommandText = "DELETE FROM CollectionTable WHERE PCGS_NUMBER = @PcgsNumber AND GRADE = @Grade";
|
cmd.CommandText = "DELETE FROM CollectionTable WHERE PCGS_NUMBER = @PcgsNumber AND GRADE = @Grade";
|
||||||
@@ -292,9 +429,18 @@ public class CoinDatabase {
|
|||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="grade">The grade string.</param>
|
||||||
private int GetGradeFromString(string grade) {
|
private int GetGradeFromString(string grade) {
|
||||||
var actualGrade = int.Parse(grade.Substring(grade.Length - 2));
|
var actualGrade = int.Parse(grade.Substring(grade.Length - 2));
|
||||||
Console.WriteLine($"Grade is {actualGrade}");
|
Console.WriteLine($"Grade is {actualGrade}");
|
||||||
return actualGrade;
|
return actualGrade;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class NotConnectedToDatabaseException : Exception {
|
||||||
|
public NotConnectedToDatabaseException() : base("Not connected to database.") { }
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using PcgsInvUi.Models;
|
using PcgsInvUi.Models;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DynamicData.Kernel;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace PcgsInvUi.Services;
|
namespace PcgsInvUi.Services;
|
||||||
@@ -27,14 +25,47 @@ public class PcgsClient : ReactiveObject {
|
|||||||
private readonly string _apiKey;
|
private readonly string _apiKey;
|
||||||
private int _numApiRequests;
|
private int _numApiRequests;
|
||||||
|
|
||||||
public PcgsClient(string apiKey) {
|
/// <summary>
|
||||||
// _apiKey = apiKey;
|
/// Creates a new instance of the PcgsClient class.
|
||||||
_apiKey =
|
/// Although this is a constructor, it's meant to be called only from the CreateClient method.
|
||||||
"E8xivTVN_k4Q8ubk8Xv_uU11U91WX0wDht4S4m4BAf2HPmKnGrZ0J2f9nccM7Ns1VkGoT1SXYR74I7aNaMKqXlJGEy3oobseKV0nGiqu1IKvFkOTVjhV37SWUVExSDFMC6FLzs7xJQIHwkcxr5JFOQNmc4ZytEh_-hmCN3NL-z0fEk98tzBmPJ_F7bq_xNru86QMD2A9yalu_dWe-lCysfyE2bCA2EDqwvWenRYf-0XnNtp6qlxxCMlCd-TDqqRKLC3qefCwpXSemwBiNSP5gbC9pwcKrZugkruuJ0nyxxThZymm";
|
/// </summary>
|
||||||
|
private PcgsClient(string apiKey) {
|
||||||
|
_apiKey = apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the PcgsClient class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiKey">The api key to use for requests.</param>
|
||||||
|
/// <returns>A new instance of the PcgsClient class, or null if the key is invalid.</returns>
|
||||||
|
public static async Task<PcgsClient?> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a request to the API to verify the credentials.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A boolean indicating whether or not the credentials are valid.</returns>
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the coin information for a given PCGS number and grade.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pcgsNumber">The PCGS number of the coin.</param>
|
||||||
|
/// <param name="grade">The grade of the coin.</param>
|
||||||
|
/// <param name="plusGrade">Whether or not the coin is a plus grade.</param>
|
||||||
|
/// <returns>A tuple containing the error type and the coin information.</returns>
|
||||||
public async Task<(ErrorType, Coin?)> GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) {
|
public async Task<(ErrorType, Coin?)> GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) {
|
||||||
Coin newCoin = null;
|
|
||||||
using (HttpClient client = new HttpClient()) {
|
using (HttpClient client = new HttpClient()) {
|
||||||
var gradeParam = GetGradeFromString(grade);
|
var gradeParam = GetGradeFromString(grade);
|
||||||
|
|
||||||
@@ -46,6 +77,7 @@ public class PcgsClient : ReactiveObject {
|
|||||||
Console.WriteLine("Got response.");
|
Console.WriteLine("Got response.");
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode) {
|
if (response.IsSuccessStatusCode) {
|
||||||
|
// Check response for errors.
|
||||||
string jsonResponse = await response.Content.ReadAsStringAsync();
|
string jsonResponse = await response.Content.ReadAsStringAsync();
|
||||||
var parsedResponse = ParseJsonResponse(jsonResponse);
|
var parsedResponse = ParseJsonResponse(jsonResponse);
|
||||||
return parsedResponse;
|
return parsedResponse;
|
||||||
@@ -55,17 +87,25 @@ public class PcgsClient : ReactiveObject {
|
|||||||
return (ErrorType.ApiKeyInvalid, null);
|
return (ErrorType.ApiKeyInvalid, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Utility function to parse the grade number from the grade string passed from the front end.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="grade">The grade string.</param>
|
||||||
|
/// <returns>The grade number.</returns>
|
||||||
private int GetGradeFromString(string grade) {
|
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}");
|
Console.WriteLine($"Grade is {actualGrade}");
|
||||||
return actualGrade;
|
return actualGrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Will eventually create the other methods that are included in the Public API.
|
/// <summary>
|
||||||
|
/// Parses the json response from the api.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="json">The json response.</param>
|
||||||
|
/// <returns>A tuple containing an error type and the coin information.</returns>
|
||||||
private (ErrorType, Coin?) ParseJsonResponse(string json) {
|
private (ErrorType, Coin?) ParseJsonResponse(string json) {
|
||||||
Console.WriteLine(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("\"ServerMessage\":\"No data found\"")) return (ErrorType.NoCoinFound, null);
|
||||||
if (json.Contains("\"IsValidRequest\":false")) return (ErrorType.InvalidRequestFormat, null);
|
if (json.Contains("\"IsValidRequest\":false")) return (ErrorType.InvalidRequestFormat, null);
|
||||||
|
|
||||||
|
|||||||
@@ -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<Unit, string> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,21 +7,10 @@ using System.Reactive.Linq;
|
|||||||
using PcgsInvUi.Models;
|
using PcgsInvUi.Models;
|
||||||
using PcgsInvUi.Services;
|
using PcgsInvUi.Services;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace PcgsInvUi.ViewModels;
|
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 class MainWindowViewModel : ViewModelBase {
|
||||||
public ViewModelBase SidebarContent {
|
public ViewModelBase SidebarContent {
|
||||||
get => _sideContent;
|
get => _sideContent;
|
||||||
@@ -50,12 +39,27 @@ public class MainWindowViewModel : ViewModelBase {
|
|||||||
|
|
||||||
public MainWindowViewModel(CoinDatabase coins) {
|
public MainWindowViewModel(CoinDatabase coins) {
|
||||||
ConnectedCoinDatabase = coins;
|
ConnectedCoinDatabase = coins;
|
||||||
|
var keyFound = Task.Run(() => coins.TryInitApiClient()).Result;
|
||||||
|
Console.WriteLine("Function completed.");
|
||||||
var newViewModel = new NewViewModel();
|
var newViewModel = new NewViewModel();
|
||||||
_sideContent = newViewModel;
|
var apiKeyViewModel = new ApiKeyViewModel();
|
||||||
// DisplayedList = CoinCollection = new ObservableCollection<Coin>(coins.GetItems());
|
// DisplayedList = CoinCollection = new ObservableCollection<Coin>(coins.GetItems());
|
||||||
DisplayedList = ConnectedCoinDatabase.Collection;
|
DisplayedList = ConnectedCoinDatabase.Collection;
|
||||||
TotalValue = DisplayedList.Sum(x => x.TotalPrice);
|
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.
|
// Set up the ability to show the error window.
|
||||||
ShowErrorWindow = new Interaction<ErrorWindowViewModel, bool>();
|
ShowErrorWindow = new Interaction<ErrorWindowViewModel, bool>();
|
||||||
|
|
||||||
@@ -113,7 +117,8 @@ public class MainWindowViewModel : ViewModelBase {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Clear the text boxes.
|
// Clear the text boxes.
|
||||||
|
newViewModel.ClearText();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update TotalValue anytime a coin's quantity changes.
|
// Update TotalValue anytime a coin's quantity changes.
|
||||||
@@ -126,4 +131,11 @@ public class MainWindowViewModel : ViewModelBase {
|
|||||||
TotalValue = ConnectedCoinDatabase.Collection.Sum(x => x.TotalPrice);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -40,9 +40,7 @@ public class NewViewModel : ViewModelBase {
|
|||||||
okEnabled);
|
okEnabled);
|
||||||
|
|
||||||
ClearCommand = ReactiveCommand.Create(() => {
|
ClearCommand = ReactiveCommand.Create(() => {
|
||||||
PcgsNumber = "";
|
ClearText();
|
||||||
Grade = "";
|
|
||||||
Quantity = "";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize grade values for use in the view.
|
// Initialize grade values for use in the view.
|
||||||
@@ -83,4 +81,13 @@ public class NewViewModel : ViewModelBase {
|
|||||||
|
|
||||||
public ReactiveCommand<Unit, (int, string, int)> OkCommand { get; }
|
public ReactiveCommand<Unit, (int, string, int)> OkCommand { get; }
|
||||||
public ReactiveCommand<Unit, Unit> ClearCommand { get; }
|
public ReactiveCommand<Unit, Unit> ClearCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the text fields.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearText() {
|
||||||
|
PcgsNumber = "";
|
||||||
|
Grade = "";
|
||||||
|
Quantity = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:viewModels="clr-namespace:PcgsInvUi.ViewModels"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="PcgsInvUi.Views.ApiKeyView" x:DataType="viewModels:ApiKeyViewModel" >
|
||||||
|
<StackPanel>
|
||||||
|
<Label Content="API key was not found or did not authenticate." />
|
||||||
|
<Label Content="Please enter your API key below:" />
|
||||||
|
<TextBox Margin="10" Text="{Binding ApiKeyInput}" />
|
||||||
|
<Button HorizontalAlignment="Center" Content="Submit" Command="{Binding OkCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace PcgsInvUi.Views;
|
||||||
|
|
||||||
|
public partial class ApiKeyView : UserControl {
|
||||||
|
public ApiKeyView() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
@@ -36,6 +35,7 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel> {
|
|||||||
|
|
||||||
private async Task ShowFilePickerAsync(InteractionContext<Unit, Uri> interationContext) {
|
private async Task ShowFilePickerAsync(InteractionContext<Unit, Uri> interationContext) {
|
||||||
var topLevel = GetTopLevel(this);
|
var topLevel = GetTopLevel(this);
|
||||||
|
if (topLevel is null) throw new Exception("Could not find top level of the file path.");
|
||||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions {
|
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions {
|
||||||
Title = "Save CSV...",
|
Title = "Save CSV...",
|
||||||
DefaultExtension = ".csv",
|
DefaultExtension = ".csv",
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using Avalonia;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace PcgsInvUi.Views;
|
namespace PcgsInvUi.Views;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
A coin collection inventory app, written in C# with the Avalonia UI framework utilizing the PCGS Public API.
|
A coin collection inventory app, written in C# with the Avalonia UI framework utilizing the PCGS Public API.
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
[.NET Core >= 6](https://dotnet.microsoft.com/en-us/download) - you will be prompted for install before running the program if it is not installed.
|
[.NET Runtime >= 6](https://dotnet.microsoft.com/en-us/download) - you will be prompted for install before running the program if it is not installed.
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
Extract contents of the downloaded file to an accessible folder (ie. Documents/PCGSApp)
|
Extract contents of the downloaded file to an accessible folder (ie. Documents/PCGSApp)
|
||||||
@@ -26,3 +26,12 @@ in the lower right corner) will update automatically.
|
|||||||
|
|
||||||
To export into a CSV file, click the Export to CSV button in the lower right corner. This will allow you to choose a
|
To export into a CSV file, click the Export to CSV button in the lower right corner. This will allow you to choose a
|
||||||
location and name for the file. These files are usable with any application that can open a .csv file, like Excel.
|
location and name for the file. These files are usable with any application that can open a .csv file, like Excel.
|
||||||
|
|
||||||
|
# Contributions
|
||||||
|
|
||||||
|
If you would like to help development, and don't mind looking through my spaghetti code (I'm still learning how to structure this)
|
||||||
|
then feel free to submit a pull request! I'm happy to take any help that I can get.
|
||||||
|
|
||||||
|
I've just started utilizing makefiles, however, since I'm currently using Windows, I'm not completely sure if there are formatting
|
||||||
|
changes that should be made to that. I will leave makefile creation for other platforms up to someone that uses these other
|
||||||
|
operating systems for now.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
build:
|
||||||
|
dotnet build .\PcgsInvUi\PcgsInvUi.csproj
|
||||||
|
|
||||||
|
run:
|
||||||
|
dotnet run --project .\PcgsInvUi\PcgsInvUi.csproj
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=C_003A_005CProgram_0020Files_005Cdotnet_005Cpacks_005CMicrosoft_002ENETCore_002EApp_002ERef_005C7_002E0_002E13_005Cref_005Cnet7_002E0_005CSystem_002EData_002Edll/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
Reference in new issue
Block a user