Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f1eb8c65 | ||
|
|
3a819ce180 | ||
|
|
0a0ea86d25 | ||
|
|
33472c727d | ||
|
|
a04cce4817 | ||
|
|
5e516fc333 | ||
|
|
0ff7edfed6 | ||
|
|
af4f7567b4 | ||
|
|
ba04d31c3e | ||
|
|
683a2cf87b | ||
|
|
fab011b898 | ||
|
|
2b4ed5374a | ||
|
|
7b3af8f7f1 | ||
|
|
c0b884fa1d | ||
|
|
88d209ab83 | ||
|
|
7eb096767c | ||
|
|
4b2504b48c | ||
|
|
435d01ef90 | ||
|
|
e21ed50dc5 | ||
|
|
a0da899537 | ||
|
|
58a3bc1c7d | ||
|
|
5433ee6719 | ||
|
|
f0849cb1f9 | ||
|
|
e17d467fb1 | ||
|
|
1145f3ef5e | ||
|
|
71673206db | ||
|
|
f98ff942bb | ||
|
|
83a08edb2e | ||
|
|
602e9bbd8b | ||
|
|
e73f9db02c | ||
|
|
d667e86b1b | ||
|
|
bc8f5e4d54 |
No files matched your search
Generated
-13
@@ -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
|
||||
Generated
-18
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AvaloniaProject">
|
||||
<option name="projectPerEditor">
|
||||
<map>
|
||||
<entry key="PcgsInvUi/App.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
<entry key="PcgsInvUi/Views/CoinListView.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
<entry key="PcgsInvUi/Views/DeleteWindow.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
<entry key="PcgsInvUi/Views/ErrorWindow.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
<entry key="PcgsInvUi/Views/FindWindow.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
<entry key="PcgsInvUi/Views/MainWindow.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
<entry key="PcgsInvUi/Views/MainWindowOld.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
<entry key="PcgsInvUi/Views/NewControl.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
<entry key="PcgsInvUi/Views/NewView.axaml" value="PcgsInvUi/PcgsInvUi.csproj" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-7
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-4
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ReactiveUI" Version="23.2.28" />
|
||||
<PackageReference Include="ReactiveUI.SourceGenerators" Version="3.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace PcgsApi;
|
||||
|
||||
public partial class PcgsClient : ReactiveObject {
|
||||
public enum ErrorType {
|
||||
None,
|
||||
NoCoinFound,
|
||||
InvalidRequestFormat,
|
||||
ApiKeyInvalid
|
||||
}
|
||||
|
||||
protected readonly string _apiKey;
|
||||
|
||||
protected readonly string _apiUri = "https://api.pcgs.com/publicapi";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the PcgsClient class.
|
||||
/// Although this is a constructor, it's meant to be called only from the CreateClient method.
|
||||
/// </summary>
|
||||
private PcgsClient(string apiKey) {
|
||||
_apiKey = apiKey;
|
||||
}
|
||||
|
||||
[Reactive] private int _numApiRequests;
|
||||
|
||||
/// <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 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, PcgsCoin?)>
|
||||
GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) {
|
||||
using (var client = new HttpClient()) {
|
||||
var gradeParam = GetGradeFromString(grade);
|
||||
|
||||
var requestUri =
|
||||
$"{_apiUri}/coindetail/GetCoinFactsByGrade/?PCGSNo={pcgsNumber}&GradeNo={gradeParam}&PlusGrade={plusGrade}";
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", _apiKey);
|
||||
Console.WriteLine($"Sending request to {requestUri}");
|
||||
var response = await client.GetAsync(requestUri);
|
||||
Console.WriteLine("Got response.");
|
||||
|
||||
if (response.IsSuccessStatusCode) {
|
||||
// Check response for errors.
|
||||
var jsonResponse = await response.Content.ReadAsStringAsync();
|
||||
var parsedResponse = ParseJsonResponse(jsonResponse);
|
||||
return parsedResponse;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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, PcgsCoin?) ParseJsonResponse(string json) {
|
||||
Console.WriteLine(json);
|
||||
// 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<PcgsCoin>(json);
|
||||
return (ErrorType.None, newCoin);
|
||||
}
|
||||
}
|
||||
|
||||
// Exception for invalid request format
|
||||
internal 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
|
||||
internal class CoinNotFoundException : Exception {
|
||||
public CoinNotFoundException() { }
|
||||
public CoinNotFoundException(string message) : base(message) { }
|
||||
public CoinNotFoundException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reactive.Linq;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace PcgsApi;
|
||||
|
||||
public partial class PcgsCoin : ReactiveObject {
|
||||
[Reactive] private string _pcgsNumber;
|
||||
[Reactive] private string _name;
|
||||
[Reactive] private int _year;
|
||||
[Reactive] private string _denomination;
|
||||
[Reactive] private string _mintMark;
|
||||
[Reactive] private string _grade;
|
||||
[Reactive] private string _coinFactsLink;
|
||||
[Reactive] private string _majorVariety;
|
||||
[Reactive] private string _minorVariety;
|
||||
[Reactive] private string _dieVariety;
|
||||
[Reactive] private string _seriesName;
|
||||
[Reactive] private string _category;
|
||||
[Reactive] private string _designation;
|
||||
[Reactive] private int _certificateNumber;
|
||||
[Reactive] private double _paidFor;
|
||||
[Reactive] private string _notes;
|
||||
[Reactive] private int _quantity;
|
||||
[Reactive] private double _priceGuideValue;
|
||||
[ObservableAsProperty] private double _totalPrice;
|
||||
|
||||
public PcgsCoin() {
|
||||
_totalPriceHelper = this.WhenAnyValue(
|
||||
x => x.Quantity,
|
||||
x => x.PriceGuideValue,
|
||||
(quantity, price) => quantity * price
|
||||
)
|
||||
.Do(_ => Debug.WriteLine("PcgsCoin total price calculated"))
|
||||
.ToProperty(this, nameof(TotalPrice));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="PcgsInv.App"
|
||||
xmlns:local="using:PcgsInv"
|
||||
RequestedThemeVariant="Light">
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator />
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -0,0 +1,22 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using PcgsInv.ViewModels;
|
||||
using PcgsInv.Views;
|
||||
|
||||
namespace PcgsInv;
|
||||
|
||||
public class App : Application {
|
||||
public override void Initialize() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted() {
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
desktop.MainWindow = new MainWindow {
|
||||
DataContext = new MainWindowViewModel()
|
||||
};
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@@ -0,0 +1,47 @@
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using PcgsApi;
|
||||
|
||||
namespace PcgsInv.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Contains all information related to a collection of coins.
|
||||
/// </summary>
|
||||
public partial class CoinCollection : ReactiveObject {
|
||||
/// <summary>
|
||||
/// The name of the collection.
|
||||
/// </summary>
|
||||
[Reactive] private string _name;
|
||||
|
||||
/// <summary>
|
||||
/// The collection of <see cref="PcgsCoin"/> objects.
|
||||
/// </summary>
|
||||
[Reactive] private ObservableCollection<PcgsCoin> _coins;
|
||||
|
||||
/// <summary>
|
||||
/// The total value of the collection.
|
||||
/// </summary>
|
||||
[ObservableAsProperty] private double _totalValue;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new CoinCollection instance.
|
||||
/// </summary>
|
||||
public CoinCollection() {
|
||||
Name = string.Empty;
|
||||
Coins = [];
|
||||
|
||||
Coins.ToObservableChangeSet()
|
||||
.AutoRefresh(x => x.TotalPrice)
|
||||
.ToCollection()
|
||||
.Select(coins => coins.Sum(c => c.TotalPrice))
|
||||
.ToProperty(this, nameof(TotalValue), out _totalValueHelper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="12.0.4"/>
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.0.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.0.4"/>
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.4"/>
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.4"/>
|
||||
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ReactiveUI" Version="23.2.28" />
|
||||
<PackageReference Include="ReactiveUI.Avalonia" Version="12.0.3" />
|
||||
<PackageReference Include="ReactiveUI.SourceGenerators" Version="3.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Converters\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PcgsApi\PcgsApi.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using ReactiveUI.Avalonia;
|
||||
|
||||
namespace PcgsInv;
|
||||
|
||||
internal sealed 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);
|
||||
}
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.UseReactiveUI(builder => builder.WithAvalonia())
|
||||
#if DEBUG
|
||||
.WithDeveloperTools()
|
||||
#endif
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using PcgsInv.ViewModels;
|
||||
|
||||
namespace PcgsInv;
|
||||
|
||||
/// <summary>
|
||||
/// Given a view model, returns the corresponding view if possible.
|
||||
/// </summary>
|
||||
[RequiresUnreferencedCode(
|
||||
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
|
||||
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
|
||||
public class ViewLocator : IDataTemplate {
|
||||
public Control? Build(object? param) {
|
||||
if (param is null)
|
||||
return null;
|
||||
|
||||
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
|
||||
var type = Type.GetType(name);
|
||||
|
||||
if (type != null) return (Control)Activator.CreateInstance(type)!;
|
||||
|
||||
return new TextBlock { Text = "Not Found: " + name };
|
||||
}
|
||||
|
||||
public bool Match(object? data) {
|
||||
return data is ViewModelBase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using PcgsApi;
|
||||
using PcgsInv.Models;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace PcgsInv.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ViewModelBase {
|
||||
[Reactive] private CoinCollection _collection;
|
||||
[Reactive] private PcgsCoin? _selectedCoin;
|
||||
|
||||
public MainWindowViewModel() {
|
||||
Collection = new CoinCollection();
|
||||
_selectedCoin = null;
|
||||
|
||||
Collection.PropertyChanged += (s, e) => {
|
||||
Debug.WriteLine($"Collection raised event: {e.PropertyName}");
|
||||
};
|
||||
|
||||
this.WhenAnyValue(x => x.Collection.TotalValue)
|
||||
.Subscribe(x => Debug.WriteLine($"Total value was recomputed: {x}"));
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private void CreateTestingCoin() {
|
||||
Collection.Coins.Add(new PcgsCoin());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using ReactiveUI;
|
||||
|
||||
namespace PcgsInv.ViewModels;
|
||||
|
||||
public abstract class ViewModelBase : ReactiveObject { }
|
||||
@@ -0,0 +1,64 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PcgsInv.ViewModels"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
Height="450" Width="800"
|
||||
x:Class="PcgsInv.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Icon="/Assets/avalonia-logo.ico"
|
||||
Title="{Binding Collection.Name, StringFormat='PCGS Inventory - {0}'}">
|
||||
|
||||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid ColumnDefinitions="3*, Auto, *"
|
||||
RowDefinitions="Auto, *">
|
||||
|
||||
<Menu Grid.Row="0" Grid.ColumnSpan="2">
|
||||
<Menu.Items>
|
||||
<MenuItem Header="File">
|
||||
<MenuItem Header="Hello"/>
|
||||
</MenuItem>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
|
||||
<DockPanel Grid.Row="1" Grid.Column="0">
|
||||
<Grid DockPanel.Dock="Top" Margin="5" ColumnDefinitions="Auto, *">
|
||||
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Collection name: "/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Collection.Name}" />
|
||||
</Grid>
|
||||
|
||||
<Button DockPanel.Dock="Top"
|
||||
Command="{Binding CreateTestingCoinCommand}"
|
||||
Content="Create Test Coin"/>
|
||||
|
||||
<TextBlock DockPanel.Dock="Bottom"
|
||||
Margin="5"
|
||||
Text="{Binding Collection.TotalValue, StringFormat='Total Value: ${0}'}"/>
|
||||
|
||||
<DataGrid ItemsSource="{Binding Collection.Coins}"
|
||||
SelectedItem="{Binding SelectedCoin}"
|
||||
Margin="5">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Width="*" Header="Name" Binding="{Binding Name}"/>
|
||||
<DataGridTextColumn Header="Quantity" Binding="{Binding Quantity}"/>
|
||||
<DataGridTextColumn Header="Price" Binding="{Binding PriceGuideValue}"/>
|
||||
<DataGridTextColumn Header="Total" Binding="{Binding TotalPrice}"
|
||||
IsReadOnly="True"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</DockPanel>
|
||||
|
||||
<GridSplitter Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Background="Gray" ResizeDirection="Columns" Margin="3"/>
|
||||
|
||||
<!-- Some other content -->
|
||||
|
||||
<DockPanel Grid.Row="1" Grid.Column="2">
|
||||
<TextBlock>This is a test!</TextBlock>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace PcgsInv.Views;
|
||||
|
||||
public partial class MainWindow : Window {
|
||||
public MainWindow() {
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="PcgsInv.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -1,7 +1,6 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="PcgsInvUi.App"
|
||||
xmlns:local="using:PcgsInvUi"
|
||||
xmlns:pcgsInvUi="clr-namespace:PcgsInvUi"
|
||||
RequestedThemeVariant="Light">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
+6
-13
@@ -1,16 +1,13 @@
|
||||
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 class App : Application {
|
||||
public override void Initialize() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
@@ -20,17 +17,13 @@ public partial class App : Application {
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
||||
var coins = new CoinDatabase();
|
||||
desktop.MainWindow = new MainWindow {
|
||||
DataContext = new MainWindowViewModel(coins),
|
||||
};
|
||||
|
||||
desktop.Exit += (sender, args) => {
|
||||
coins.UpdateCollection("CollectionTable");
|
||||
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?
|
||||
}
|
||||
}
|
||||
+28
-28
@@ -5,33 +5,33 @@ project(PcgsInvUi LANGUAGES CSharp)
|
||||
include(CSharpUtilities)
|
||||
|
||||
add_executable(PcgsInvUi
|
||||
App.axaml.cs
|
||||
Program.cs
|
||||
ViewLocator.cs
|
||||
App.axaml
|
||||
app.manifest
|
||||
Models/Coin.cs
|
||||
Services/CoinCollection.cs
|
||||
Services/CoinDatabase.cs
|
||||
Services/PcgsClient.cs
|
||||
ViewModels/CoinListViewModel.cs
|
||||
ViewModels/DeleteWindowViewModel.cs
|
||||
ViewModels/EditViewModel.cs
|
||||
ViewModels/ErrorWindowViewModel.cs
|
||||
ViewModels/FindWindowViewModel.cs
|
||||
ViewModels/MainWindowViewModel.cs
|
||||
ViewModels/NewViewModel.cs
|
||||
ViewModels/ViewModelBase.cs
|
||||
Views/DeleteWindow.axaml
|
||||
Views/DeleteWindow.axaml.cs
|
||||
Views/ErrorWindow.axaml
|
||||
Views/ErrorWindow.axaml.cs
|
||||
Views/FindWindow.axaml
|
||||
Views/FindWindow.axaml.cs
|
||||
Views/MainWindow.axaml
|
||||
Views/MainWindow.axaml.cs
|
||||
Views/NewView.axaml
|
||||
Views/NewView.axaml.cs
|
||||
App.axaml.cs
|
||||
Program.cs
|
||||
ViewLocator.cs
|
||||
App.axaml
|
||||
app.manifest
|
||||
Models/Coin.cs
|
||||
Services/CoinCollection.cs
|
||||
Services/CoinDatabase.cs
|
||||
Services/PcgsClient.cs
|
||||
ViewModels/CoinListViewModel.cs
|
||||
ViewModels/DeleteWindowViewModel.cs
|
||||
ViewModels/EditViewModel.cs
|
||||
ViewModels/ErrorWindowViewModel.cs
|
||||
ViewModels/FindWindowViewModel.cs
|
||||
ViewModels/MainWindowViewModel.cs
|
||||
ViewModels/NewViewModel.cs
|
||||
ViewModels/ViewModelBase.cs
|
||||
Views/DeleteWindow.axaml
|
||||
Views/DeleteWindow.axaml.cs
|
||||
Views/ErrorWindow.axaml
|
||||
Views/ErrorWindow.axaml.cs
|
||||
Views/FindWindow.axaml
|
||||
Views/FindWindow.axaml.cs
|
||||
Views/MainWindow.axaml
|
||||
Views/MainWindow.axaml.cs
|
||||
Views/NewView.axaml
|
||||
Views/NewView.axaml.cs
|
||||
)
|
||||
|
||||
# Set the .NET Framework version for the executable.
|
||||
@@ -43,5 +43,5 @@ set(CMAKE_CSharp_FLAGS "/langversion:latest")
|
||||
|
||||
# Add the .NET reference libraries as dependencies.
|
||||
set_property(TARGET PcgsInvUi PROPERTY VS_DOTNET_REFERENCES
|
||||
"Microsoft.CSharp"
|
||||
"Microsoft.CSharp"
|
||||
)
|
||||
@@ -1,165 +0,0 @@
|
||||
using ReactiveUI;
|
||||
|
||||
namespace PcgsInvUi.Models;
|
||||
|
||||
public class Coin : ReactiveObject {
|
||||
public string PCGSNo {
|
||||
get => _pcgsNo;
|
||||
set => this.RaiseAndSetIfChanged(ref _pcgsNo, value);
|
||||
}
|
||||
|
||||
private string _pcgsNo;
|
||||
|
||||
public string Name {
|
||||
get => _name;
|
||||
set => this.RaiseAndSetIfChanged(ref _name, value);
|
||||
}
|
||||
|
||||
private string _name;
|
||||
|
||||
public int Year {
|
||||
get => _year;
|
||||
set => this.RaiseAndSetIfChanged(ref _year, value);
|
||||
}
|
||||
|
||||
private int _year;
|
||||
|
||||
public string Denomination {
|
||||
get => _denomination;
|
||||
set => this.RaiseAndSetIfChanged(ref _denomination, value);
|
||||
}
|
||||
|
||||
private string _denomination;
|
||||
|
||||
public string MintMark {
|
||||
get => _mintMark;
|
||||
set => this.RaiseAndSetIfChanged(ref _mintMark, value);
|
||||
}
|
||||
|
||||
private string _mintMark;
|
||||
|
||||
public string Grade {
|
||||
get => _grade;
|
||||
set => this.RaiseAndSetIfChanged(ref _grade, value);
|
||||
}
|
||||
|
||||
private string _grade;
|
||||
|
||||
public double PriceGuideValue {
|
||||
get => _priceGuideValue;
|
||||
set {
|
||||
TotalPrice = value * Quantity;
|
||||
this.RaiseAndSetIfChanged(ref _priceGuideValue, value);
|
||||
}
|
||||
}
|
||||
|
||||
private double _priceGuideValue;
|
||||
|
||||
public string CoinFactsLink {
|
||||
get => _coinFactsLink;
|
||||
set => this.RaiseAndSetIfChanged(ref _coinFactsLink, value);
|
||||
}
|
||||
|
||||
private string _coinFactsLink;
|
||||
|
||||
public string MajorVariety {
|
||||
get => _majorVariety;
|
||||
set => this.RaiseAndSetIfChanged(ref _majorVariety, value);
|
||||
}
|
||||
|
||||
private string _majorVariety;
|
||||
|
||||
public string MinorVariety {
|
||||
get => _minorVariety;
|
||||
set => this.RaiseAndSetIfChanged(ref _minorVariety, value);
|
||||
}
|
||||
|
||||
private string _minorVariety;
|
||||
|
||||
public string DieVariety {
|
||||
get => _dieVariety;
|
||||
set => this.RaiseAndSetIfChanged(ref _dieVariety, value);
|
||||
}
|
||||
|
||||
private string _dieVariety;
|
||||
|
||||
public string SeriesName {
|
||||
get => _seriesName;
|
||||
set => this.RaiseAndSetIfChanged(ref _seriesName, value);
|
||||
}
|
||||
|
||||
private string _seriesName;
|
||||
|
||||
public string Category {
|
||||
get => _category;
|
||||
set => this.RaiseAndSetIfChanged(ref _category, value);
|
||||
}
|
||||
|
||||
private string _category;
|
||||
|
||||
public string Designation {
|
||||
get => _designation;
|
||||
set => this.RaiseAndSetIfChanged(ref _designation, value);
|
||||
}
|
||||
|
||||
private string _designation;
|
||||
|
||||
public int CertificateNumber {
|
||||
get => _certificateNumber;
|
||||
set => this.RaiseAndSetIfChanged(ref _certificateNumber, value);
|
||||
}
|
||||
|
||||
private int _certificateNumber;
|
||||
|
||||
public int Quantity {
|
||||
get => _quantity;
|
||||
set {
|
||||
TotalPrice = value * PriceGuideValue;
|
||||
this.RaiseAndSetIfChanged(ref _quantity, value);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
+20
-11
@@ -1,11 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<Company>BobdaFett</Company>
|
||||
<AssemblyVersion>0.0.5</AssemblyVersion>
|
||||
<LangVersion>latestmajor</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -15,16 +18,17 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.5" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.5" />
|
||||
<PackageReference Include="Avalonia" Version="12.0.4"/>
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.0.0"/>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.0.4"/>
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.4"/>
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.4"/>
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.5" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.17"/>
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.8"/>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="System.Data.SQLite" Version="1.0.118" />
|
||||
<PackageReference Include="System.Data.SQLite" Version="2.0.3"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -63,7 +67,7 @@
|
||||
<Compile Include="obj\Debug\net6.0\.NETCoreApp,Version=v6.0.AssemblyAttributes.cs"/>
|
||||
<Compile Include="obj\Debug\net6.0\PcgsInvUi.AssemblyInfo.cs"/>
|
||||
<Compile Remove="obj\**"/>
|
||||
<Compile Remove="Services\CoinCollection.cs" />
|
||||
<Compile Remove="Services\CoinCollection.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -88,6 +92,11 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Data"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Models\"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,22 +1,24 @@
|
||||
using Avalonia;
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.ReactiveUI;
|
||||
using System;
|
||||
using PcgsInvUi.ViewModels;
|
||||
|
||||
namespace PcgsInvUi;
|
||||
|
||||
class Program {
|
||||
internal 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()
|
||||
=> AppBuilder.Configure<App>()
|
||||
public static AppBuilder BuildAvaloniaApp() {
|
||||
return AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace()
|
||||
.UseReactiveUI();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
+247
-103
@@ -1,32 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
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<Coin> Collection { 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() {
|
||||
// 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<Coin>();
|
||||
Collection = new ObservableCollection<PcgsCoin>();
|
||||
GetCollection("CollectionTable");
|
||||
Console.WriteLine("Got collection.");
|
||||
}
|
||||
|
||||
public SQLiteConnection Connection { get; set; }
|
||||
private bool IsConnected { get; set; }
|
||||
public ObservableCollection<PcgsCoin> Collection { get; set; }
|
||||
public bool IsClientConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Destructor. Currently not used, due to the strange nature of desktop applications.
|
||||
/// </summary>
|
||||
~CoinDatabase() {
|
||||
Console.WriteLine("Destructor called, saving collection.");
|
||||
// Save collection.
|
||||
@@ -35,19 +41,81 @@ public class CoinDatabase {
|
||||
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... ");
|
||||
var 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() {
|
||||
// Check if database exists.
|
||||
if (!System.IO.File.Exists("coin_collection")) {
|
||||
// Create the database.
|
||||
if (!File.Exists("coin_collection")) {
|
||||
// If not, create the database.
|
||||
Console.Write("Creating database... ");
|
||||
SQLiteConnection.CreateFile("coin_collection");
|
||||
SQLiteConnection.CreateFile("coin_collection.sqlite");
|
||||
Console.WriteLine("Done.");
|
||||
}
|
||||
|
||||
|
||||
// Create a connection to the database.
|
||||
SQLiteConnection connection =
|
||||
var connection =
|
||||
new SQLiteConnection("DataSource=coin_collection");
|
||||
|
||||
|
||||
// Open the connection.
|
||||
try {
|
||||
connection.Open();
|
||||
@@ -60,6 +128,33 @@ public class CoinDatabase {
|
||||
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.
|
||||
var 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) {
|
||||
// Create a table to store the collection.
|
||||
if (!IsConnected) {
|
||||
@@ -67,9 +162,9 @@ public class CoinDatabase {
|
||||
Console.WriteLine("Not connected to database.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create a SQL command.
|
||||
SQLiteCommand cmd = Connection.CreateCommand();
|
||||
var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText =
|
||||
$"CREATE TABLE IF NOT EXISTS {collectionName} (" +
|
||||
"PCGS_NUMBER TEXT," +
|
||||
@@ -90,12 +185,16 @@ public class CoinDatabase {
|
||||
"QUANTITY INTEGER," +
|
||||
"NOTES TEXT," +
|
||||
"PRIMARY KEY(PCGS_NUMBER, GRADE))";
|
||||
|
||||
|
||||
// Execute the command.
|
||||
cmd.ExecuteNonQuery();
|
||||
Console.WriteLine("Collection table initialized.");
|
||||
}
|
||||
|
||||
public void InsertCoin(Coin coin) {
|
||||
/// <summary>
|
||||
/// Inserts a Coin object into the database.
|
||||
/// </summary>
|
||||
public void InsertCoin(PcgsCoin pcgsCoin) {
|
||||
Console.WriteLine("Inserting coin...");
|
||||
// Use our existing connection to insert a coin to the database.
|
||||
if (!IsConnected) {
|
||||
@@ -103,24 +202,31 @@ public class CoinDatabase {
|
||||
Console.WriteLine("Not connected to database.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create a SQL command.
|
||||
SQLiteCommand cmd = Connection.CreateCommand();
|
||||
var 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);
|
||||
|
||||
$"VALUES ('{pcgsCoin.PcgsNumber}', '{pcgsCoin.Grade}', {pcgsCoin.Year}, '{pcgsCoin.Denomination}', '{pcgsCoin.MintMark}', {pcgsCoin.PriceGuideValue}," +
|
||||
$"'{pcgsCoin.CoinFactsLink}', '{pcgsCoin.MajorVariety}', '{pcgsCoin.MinorVariety}', '{pcgsCoin.DieVariety}', '{pcgsCoin.SeriesName}', " +
|
||||
$"'{pcgsCoin.Category}', '{pcgsCoin.Designation}', {pcgsCoin.CertificateNumber}, {pcgsCoin.Quantity}, @Notes)";
|
||||
cmd.Parameters.AddWithValue("@Notes", pcgsCoin.Notes);
|
||||
|
||||
// Execute the command.
|
||||
cmd.ExecuteNonQuery();
|
||||
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) {
|
||||
Console.WriteLine("Creating coin...");
|
||||
// Create a coin by calling the API, then insert it into the database.
|
||||
@@ -131,7 +237,7 @@ public class CoinDatabase {
|
||||
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) {
|
||||
@@ -141,53 +247,68 @@ public class CoinDatabase {
|
||||
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) {
|
||||
|
||||
/// <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 PcgsCoin? GetCoin(int pcgsNumber, string grade) {
|
||||
Console.WriteLine($"Getting coin {pcgsNumber}, {grade}...");
|
||||
int gradeInt = GetGradeFromString(grade);
|
||||
|
||||
var 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.
|
||||
foreach (var coin in Collection)
|
||||
if (coin.PcgsNumber == 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.
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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) {
|
||||
Console.WriteLine("Getting collection...");
|
||||
SQLiteCommand cmd = Connection.CreateCommand();
|
||||
var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText = $"SELECT * FROM {collectionName}";
|
||||
|
||||
|
||||
using (var reader = cmd.ExecuteReader()) {
|
||||
while (reader.Read()) {
|
||||
while (reader.Read())
|
||||
// Create coin from reader then add to list.
|
||||
Collection.Add(CoinFromReader(reader));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String CollectionToCSV(string collectionName) {
|
||||
/// <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) {
|
||||
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()) {
|
||||
@@ -202,23 +323,20 @@ public class CoinDatabase {
|
||||
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++) {
|
||||
for (var i = 0; i < numHeaders; i++) {
|
||||
// Get the type of each column.
|
||||
var type = reader.GetFieldType(i);
|
||||
Console.WriteLine("Type: " + type);
|
||||
if (type == typeof(Int64)) {
|
||||
if (type == typeof(long))
|
||||
resultString += reader.GetInt32(i) + ",";
|
||||
}
|
||||
else if (type == typeof(Double)) {
|
||||
else if (type == typeof(double))
|
||||
resultString += reader.GetDouble(i) + ",";
|
||||
}
|
||||
else if (type == typeof(String)) {
|
||||
else if (type == typeof(string))
|
||||
resultString += reader.GetString(i) + ",";
|
||||
}
|
||||
else {
|
||||
else
|
||||
resultString += "NULL" + ",";
|
||||
}
|
||||
}
|
||||
|
||||
resultString += "\n";
|
||||
}
|
||||
}
|
||||
@@ -226,75 +344,101 @@ public class CoinDatabase {
|
||||
return resultString;
|
||||
}
|
||||
|
||||
public void SaveCoin(string collectionName, Coin coin) {
|
||||
/// <summary>
|
||||
/// Saves a Coin object to the database.
|
||||
/// </summary>
|
||||
/// <param name="collectionName">The name of the collection table.</param>
|
||||
/// <param name="pcgsCoin">The coin to save.</param>
|
||||
public void SaveCoin(string collectionName, PcgsCoin pcgsCoin) {
|
||||
// Get and replace the coin from the database.
|
||||
Console.WriteLine("Saving coin...");
|
||||
SQLiteCommand cmd = Connection.CreateCommand();
|
||||
var 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);
|
||||
$"SET YEAR = {pcgsCoin.Year}, " +
|
||||
$"DENOMINATION = \'{pcgsCoin.Denomination}\', " +
|
||||
$"MINT_MARK = \'{pcgsCoin.MintMark}\', " +
|
||||
$"PRICE_GUIDE_VALUE = \'{pcgsCoin.PriceGuideValue}\', " +
|
||||
$"COIN_FACTS_LINK = \'{pcgsCoin.CoinFactsLink}\', " +
|
||||
$"MAJOR_VARIETY = \'{pcgsCoin.MajorVariety}\', " +
|
||||
$"MINOR_VARIETY = \'{pcgsCoin.MinorVariety}\', " +
|
||||
$"DIE_VARIETY = \'{pcgsCoin.DieVariety}\', " +
|
||||
$"SERIES_NAME = \'{pcgsCoin.SeriesName}\', " +
|
||||
$"CATEGORY = \'{pcgsCoin.Category}\', " +
|
||||
$"DESIGNATION = \'{pcgsCoin.Designation}\', " +
|
||||
$"CERT_NUM = \'{pcgsCoin.CertificateNumber}\', " +
|
||||
$"QUANTITY = {pcgsCoin.Quantity}, " +
|
||||
$"NOTES = @Notes " +
|
||||
$"WHERE PCGS_NUMBER = {pcgsCoin.PcgsNumber} AND GRADE = \'{pcgsCoin.Grade}\'";
|
||||
cmd.Parameters.AddWithValue("@Notes", pcgsCoin.Notes);
|
||||
Console.WriteLine(cmd.CommandText);
|
||||
// Execute the command.
|
||||
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) {
|
||||
// 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.
|
||||
}
|
||||
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) {
|
||||
|
||||
/// <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 PcgsCoin 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;
|
||||
PcgsCoin pcgsCoin = new PcgsCoin();
|
||||
if (!reader.IsDBNull(0)) pcgsCoin.PcgsNumber = reader.GetString(0);
|
||||
if (!reader.IsDBNull(1)) pcgsCoin.Grade = reader.GetString(1);
|
||||
if (!reader.IsDBNull(2)) pcgsCoin.Year = reader.GetInt32(2);
|
||||
if (!reader.IsDBNull(3)) pcgsCoin.Denomination = reader.GetString(3);
|
||||
if (!reader.IsDBNull(4)) pcgsCoin.MintMark = reader.GetString(4);
|
||||
if (!reader.IsDBNull(5)) pcgsCoin.PriceGuideValue = reader.GetDouble(5);
|
||||
if (!reader.IsDBNull(6)) pcgsCoin.CoinFactsLink = reader.GetString(6);
|
||||
if (!reader.IsDBNull(7)) pcgsCoin.MajorVariety = reader.GetString(7);
|
||||
if (!reader.IsDBNull(8)) pcgsCoin.MinorVariety = reader.GetString(8);
|
||||
if (!reader.IsDBNull(9)) pcgsCoin.DieVariety = reader.GetString(9);
|
||||
if (!reader.IsDBNull(10)) pcgsCoin.SeriesName = reader.GetString(10);
|
||||
if (!reader.IsDBNull(11)) pcgsCoin.Category = reader.GetString(11);
|
||||
if (!reader.IsDBNull(12)) pcgsCoin.Designation = reader.GetString(12);
|
||||
if (!reader.IsDBNull(13)) pcgsCoin.CertificateNumber = reader.GetInt32(13);
|
||||
if (!reader.IsDBNull(14)) pcgsCoin.Quantity = reader.GetInt32(14);
|
||||
if (!reader.IsDBNull(15)) pcgsCoin.Notes = reader.GetString(15);
|
||||
// pcgsCoin.TotalPrice = pcgsCoin.Quantity * pcgsCoin.PriceGuideValue;
|
||||
return pcgsCoin;
|
||||
}
|
||||
|
||||
public void DeleteCoin(Coin delCoin) {
|
||||
SQLiteCommand cmd = Connection.CreateCommand();
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a Coin object from the database.
|
||||
/// </summary>
|
||||
/// <param name="delPcgsCoin">The coin to delete.</param>
|
||||
public void DeleteCoin(PcgsCoin delPcgsCoin) {
|
||||
var 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.Parameters.AddWithValue("@PcgsNumber", delPcgsCoin.PcgsNumber);
|
||||
cmd.Parameters.AddWithValue("@Grade", delPcgsCoin.Grade);
|
||||
|
||||
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) {
|
||||
var actualGrade = int.Parse(grade.Substring(grade.Length - 2));
|
||||
Console.WriteLine($"Grade is {actualGrade}");
|
||||
return actualGrade;
|
||||
}
|
||||
}
|
||||
|
||||
public class NotConnectedToDatabaseException : Exception {
|
||||
public NotConnectedToDatabaseException() : base("Not connected to database.") { }
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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;
|
||||
|
||||
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;
|
||||
_apiKey =
|
||||
"E8xivTVN_k4Q8ubk8Xv_uU11U91WX0wDht4S4m4BAf2HPmKnGrZ0J2f9nccM7Ns1VkGoT1SXYR74I7aNaMKqXlJGEy3oobseKV0nGiqu1IKvFkOTVjhV37SWUVExSDFMC6FLzs7xJQIHwkcxr5JFOQNmc4ZytEh_-hmCN3NL-z0fEk98tzBmPJ_F7bq_xNru86QMD2A9yalu_dWe-lCysfyE2bCA2EDqwvWenRYf-0XnNtp6qlxxCMlCd-TDqqRKLC3qefCwpXSemwBiNSP5gbC9pwcKrZugkruuJ0nyxxThZymm";
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
var parsedResponse = ParseJsonResponse(jsonResponse);
|
||||
return parsedResponse;
|
||||
}
|
||||
}
|
||||
|
||||
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 (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<Coin>(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) { }
|
||||
}
|
||||
@@ -10,9 +10,7 @@ public class ViewLocator : IDataTemplate {
|
||||
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||
var type = Type.GetType(name);
|
||||
|
||||
if (type != null) {
|
||||
return (Control)Activator.CreateInstance(type)!;
|
||||
}
|
||||
if (type != null) return (Control)Activator.CreateInstance(type)!;
|
||||
|
||||
return new TextBlock { Text = "Not Found: " + name };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace PcgsInvUi.ViewModels;
|
||||
|
||||
public partial class ApiKeyViewModel : ViewModelBase {
|
||||
[ObservableProperty]
|
||||
public partial string ApiKeyInput { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool OkEnabled { get; set; }
|
||||
|
||||
[RelayCommand]
|
||||
public void OkCommand() {
|
||||
Debug.WriteLine("Running OkCommand from ApiKeyViewModel");
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using PcgsInvUi.Models;
|
||||
using ReactiveUI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace PcgsInvUi.ViewModels;
|
||||
|
||||
public class CoinListViewModel : ViewModelBase {
|
||||
public ObservableCollection<Coin> CoinCollection { get; set; }
|
||||
private float _totalValue;
|
||||
|
||||
public float TotalValue {
|
||||
get => _totalValue;
|
||||
set => this.RaiseAndSetIfChanged(ref _totalValue, value);
|
||||
}
|
||||
|
||||
public Coin? SelectedCoin { get; set; }
|
||||
|
||||
public CoinListViewModel(IEnumerable<Coin> items) {
|
||||
CoinCollection = new ObservableCollection<Coin>(items);
|
||||
public partial class CoinListViewModel : ViewModelBase {
|
||||
public CoinListViewModel(IEnumerable<PcgsCoin> items) {
|
||||
CoinCollection = new ObservableCollection<PcgsCoin>(items);
|
||||
TotalValue = 0;
|
||||
}
|
||||
|
||||
public ObservableCollection<PcgsCoin> CoinCollection { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial float TotalValue { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial PcgsCoin? SelectedCoin { get; set; }
|
||||
}
|
||||
@@ -4,11 +4,11 @@ using ReactiveUI;
|
||||
namespace PcgsInvUi.ViewModels;
|
||||
|
||||
public class DeleteWindowViewModel : ViewModelBase {
|
||||
public ReactiveCommand<Unit, object> OkCommand { get; }
|
||||
public ReactiveCommand<Unit, object> CancelCommand { get; }
|
||||
|
||||
public DeleteWindowViewModel() {
|
||||
OkCommand = ReactiveCommand.Create(() => (object)true);
|
||||
CancelCommand = ReactiveCommand.Create(() => (object)false);
|
||||
}
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, object> OkCommand { get; }
|
||||
public ReactiveCommand<Unit, object> CancelCommand { get; }
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
using System.Reactive;
|
||||
using PcgsInvUi.Models;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace PcgsInvUi.ViewModels;
|
||||
|
||||
public class EditViewModel : ViewModelBase {
|
||||
public Coin CurrentCoin { get; set; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> OkCommand { get; }
|
||||
|
||||
public EditViewModel(Coin coin) {
|
||||
CurrentCoin = coin;
|
||||
public EditViewModel(PcgsCoin pcgsCoin) {
|
||||
CurrentPcgsCoin = pcgsCoin;
|
||||
|
||||
var okEnabled = this.WhenAnyValue(
|
||||
x => x.CurrentCoin.PCGSNo,
|
||||
x => x.CurrentCoin.Grade,
|
||||
x => x.CurrentCoin.Quantity,
|
||||
x => x.CurrentPcgsCoin.PcgsNumber,
|
||||
x => x.CurrentPcgsCoin.Grade,
|
||||
x => x.CurrentPcgsCoin.Quantity,
|
||||
(id, grade, quantity) => !string.IsNullOrWhiteSpace(id) &&
|
||||
!string.IsNullOrWhiteSpace(grade) &&
|
||||
(quantity != 0)
|
||||
quantity != 0
|
||||
);
|
||||
|
||||
// Setup OkCommand - all Coin-related properties must be non-whitespace.
|
||||
OkCommand = ReactiveCommand.Create(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
public PcgsCoin CurrentPcgsCoin { get; set; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> OkCommand { get; }
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
using System.Reactive;
|
||||
using ReactiveUI;
|
||||
using Avalonia.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace PcgsInvUi.ViewModels;
|
||||
namespace PcgsInvUi.ViewModels;
|
||||
|
||||
public class ErrorWindowViewModel : ViewModelBase {
|
||||
public partial class ErrorWindowViewModel : ViewModelBase {
|
||||
private readonly Window _dialog;
|
||||
|
||||
public string DisplayedMessage {
|
||||
get => _message;
|
||||
set => this.RaiseAndSetIfChanged(ref _message, value);
|
||||
}
|
||||
private string _message;
|
||||
|
||||
public ReactiveCommand<Unit, object> CloseCommand { get; }
|
||||
|
||||
public ErrorWindowViewModel(string message)
|
||||
{
|
||||
_message = message;
|
||||
|
||||
CloseCommand = ReactiveCommand.Create(() => (object)false);
|
||||
public ErrorWindowViewModel(Window dialog, string message) {
|
||||
_dialog = dialog;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
}
|
||||
[ObservableProperty]
|
||||
public partial string Message { get; set; }
|
||||
|
||||
[RelayCommand]
|
||||
public void Close() {
|
||||
_dialog.Close();
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using PcgsInvUi.Models;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using PcgsInvUi.Services;
|
||||
using ReactiveUI;
|
||||
|
||||
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;
|
||||
private set => this.RaiseAndSetIfChanged(ref _sideContent, value);
|
||||
}
|
||||
public ObservableCollection<Coin> 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 ReactiveCommand<Unit, Unit> DeleteCommand { get; }
|
||||
public Interaction<DeleteWindowViewModel, Boolean> ShowDeleteWindow { get; }
|
||||
public ReactiveCommand<Unit, Unit> ExportCommand { get; }
|
||||
public Interaction<Unit, Uri> ShowExportWindow { get; }
|
||||
public Interaction<ErrorWindowViewModel, bool> ShowErrorWindow { get; }
|
||||
|
||||
private ViewModelBase _sideContent;
|
||||
private Coin? _selectedCoin;
|
||||
private double _totalValue;
|
||||
|
||||
public partial class MainWindowViewModel : ViewModelBase {
|
||||
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<Coin>(coins.GetItems());
|
||||
DisplayedList = ConnectedCoinDatabase.Collection;
|
||||
TotalValue = DisplayedList.Sum(x => x.TotalPrice);
|
||||
|
||||
|
||||
// Setup interaction to show ApiKeyWindow
|
||||
apiKeyViewModel.OkCommandCommand.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.
|
||||
SidebarContent = apiKeyViewModel;
|
||||
}
|
||||
else {
|
||||
SidebarContent = newViewModel;
|
||||
}
|
||||
|
||||
// Set up the ability to show the error window.
|
||||
ShowErrorWindow = new Interaction<ErrorWindowViewModel, bool>();
|
||||
|
||||
|
||||
// Set up the ability to open the delete window.
|
||||
ShowDeleteWindow = new Interaction<DeleteWindowViewModel, bool>();
|
||||
var deleteEnabled = this.WhenAnyValue(x => x.SelectedCoin)
|
||||
@@ -85,23 +67,25 @@ public class MainWindowViewModel : ViewModelBase {
|
||||
// Something is wrong with the file after it's been created.
|
||||
Console.WriteLine(e.Message);
|
||||
await ShowErrorWindow.Handle(new ErrorWindowViewModel(e.Message));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
catch (Exception e) {
|
||||
Console.WriteLine(e.Message);
|
||||
await 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."));
|
||||
await ShowErrorWindow.Handle(
|
||||
new ErrorWindowViewModel("API key is invalid - authorization failed."));
|
||||
break;
|
||||
case PcgsClient.ErrorType.InvalidRequestFormat:
|
||||
Console.WriteLine("Request format is invalid - showing window.");
|
||||
@@ -109,13 +93,15 @@ public class MainWindowViewModel : ViewModelBase {
|
||||
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."));
|
||||
await ShowErrorWindow.Handle(
|
||||
new ErrorWindowViewModel("No coin was found with the given parameters."));
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO Clear the text boxes.
|
||||
|
||||
// Clear the text boxes.
|
||||
newViewModel.ClearText();
|
||||
});
|
||||
|
||||
|
||||
// Update TotalValue anytime a coin's quantity changes.
|
||||
// Possible null reference is currently ignored - SelectedCoin will never be null.
|
||||
this.WhenAnyValue(x => x.SelectedCoin.TotalPrice)
|
||||
@@ -126,4 +112,29 @@ public class MainWindowViewModel : ViewModelBase {
|
||||
TotalValue = ConnectedCoinDatabase.Collection.Sum(x => x.TotalPrice);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ViewModelBase SidebarContent { get; private set; }
|
||||
|
||||
public ObservableCollection<PcgsCoin> DisplayedList { get; set; }
|
||||
public CoinDatabase ConnectedCoinDatabase { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial PcgsCoin? SelectedCoin { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double TotalValue { get; set; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> DeleteCommand { get; }
|
||||
public Interaction<DeleteWindowViewModel, bool> ShowDeleteWindow { get; }
|
||||
public ReactiveCommand<Unit, Unit> ExportCommand { get; }
|
||||
public Interaction<Unit, Uri> ShowExportWindow { get; }
|
||||
public Interaction<ErrorWindowViewModel, bool> ShowErrorWindow { get; }
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,14 @@
|
||||
using System.Reactive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace PcgsInvUi.ViewModels;
|
||||
|
||||
public class NewViewModel : ViewModelBase {
|
||||
private string _pcgsNumber = "";
|
||||
private string _grade = "";
|
||||
private string _pcgsNumber = "";
|
||||
private string _quantity = "";
|
||||
|
||||
public string PcgsNumber {
|
||||
get => _pcgsNumber;
|
||||
set => this.RaiseAndSetIfChanged(ref _pcgsNumber, value);
|
||||
}
|
||||
|
||||
public string Grade {
|
||||
get => _grade;
|
||||
set => this.RaiseAndSetIfChanged(ref _grade, value);
|
||||
}
|
||||
|
||||
public string Quantity {
|
||||
get => _quantity;
|
||||
set => this.RaiseAndSetIfChanged(ref _quantity, value);
|
||||
}
|
||||
|
||||
public List<string> GradesList { get; }
|
||||
|
||||
public NewViewModel() {
|
||||
var okEnabled = this.WhenAnyValue(
|
||||
x => x.PcgsNumber,
|
||||
@@ -39,11 +22,7 @@ public class NewViewModel : ViewModelBase {
|
||||
() => (int.Parse(PcgsNumber), Grade, int.Parse(Quantity)),
|
||||
okEnabled);
|
||||
|
||||
ClearCommand = ReactiveCommand.Create(() => {
|
||||
PcgsNumber = "";
|
||||
Grade = "";
|
||||
Quantity = "";
|
||||
});
|
||||
ClearCommand = ReactiveCommand.Create(() => { ClearText(); });
|
||||
|
||||
// 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.
|
||||
@@ -81,6 +60,32 @@ public class NewViewModel : ViewModelBase {
|
||||
};
|
||||
}
|
||||
|
||||
public string PcgsNumber {
|
||||
get => _pcgsNumber;
|
||||
set => this.RaiseAndSetIfChanged(ref _pcgsNumber, value);
|
||||
}
|
||||
|
||||
public string Grade {
|
||||
get => _grade;
|
||||
set => this.RaiseAndSetIfChanged(ref _grade, value);
|
||||
}
|
||||
|
||||
public string Quantity {
|
||||
get => _quantity;
|
||||
set => this.RaiseAndSetIfChanged(ref _quantity, value);
|
||||
}
|
||||
|
||||
public List<string> GradesList { get; }
|
||||
|
||||
public ReactiveCommand<Unit, (int, string, int)> OkCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> ClearCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Clears the text fields.
|
||||
/// </summary>
|
||||
public void ClearText() {
|
||||
PcgsNumber = "";
|
||||
Grade = "";
|
||||
Quantity = "";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using ReactiveUI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace PcgsInvUi.ViewModels;
|
||||
|
||||
public class ViewModelBase : ReactiveObject { }
|
||||
public class ViewModelBase : ObservableObject { }
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,4 @@
|
||||
HorizontalContentAlignment="Center" IsEnabled="True"
|
||||
Content="No" Command="{Binding CancelCommand}" />
|
||||
</Grid>
|
||||
</Window>
|
||||
</Window>
|
||||
@@ -1,7 +1,7 @@
|
||||
using PcgsInvUi.ViewModels;
|
||||
using System;
|
||||
using Avalonia.ReactiveUI;
|
||||
using PcgsInvUi.ViewModels;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
|
||||
namespace PcgsInvUi.Views;
|
||||
|
||||
@@ -11,4 +11,4 @@ public partial class DeleteWindow : ReactiveWindow<DeleteWindowViewModel> {
|
||||
this.WhenActivated(d => d(ViewModel!.OkCommand.Subscribe(Close)));
|
||||
this.WhenActivated(d => d(ViewModel!.CancelCommand.Subscribe(Close)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
xmlns:viewModels="clr-namespace:PcgsInvUi.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="150"
|
||||
x:Class="PcgsInvUi.Views.ErrorWindow" x:DataType="viewModels:ErrorWindowViewModel"
|
||||
Title="Error!" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterOwner"
|
||||
x:Name="NameErrorWindow" CanResize="False" >
|
||||
Title="Error!" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterOwner"
|
||||
x:Name="NameErrorWindow" CanResize="False">
|
||||
<Grid RowDefinitions="*,*">
|
||||
<Label Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Stretch"
|
||||
Margin="15"
|
||||
Content="{Binding DisplayedMessage}" />
|
||||
Content="{Binding Message}" />
|
||||
<Button Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
||||
Margin="10" Name="CloseButton" IsEnabled="True"
|
||||
Content="OK" Command="{Binding CloseCommand, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</Window>
|
||||
</Window>
|
||||
@@ -1,14 +1,11 @@
|
||||
using System;
|
||||
using Avalonia.ReactiveUI;
|
||||
using PcgsInvUi.ViewModels;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace PcgsInvUi.Views;
|
||||
namespace PcgsInvUi.Views;
|
||||
|
||||
public partial class ErrorWindow : ReactiveWindow<ErrorWindowViewModel> {
|
||||
public ErrorWindow()
|
||||
{
|
||||
public ErrorWindow() {
|
||||
InitializeComponent();
|
||||
this.WhenActivated(d => d(ViewModel!.CloseCommand.Subscribe(Close)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia.ReactiveUI;
|
||||
using System;
|
||||
using Avalonia.ReactiveUI;
|
||||
using PcgsInvUi.ViewModels;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
|
||||
namespace PcgsInvUi.Views;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:viewModels="clr-namespace:PcgsInvUi.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="PcgsInvUi.Views.MainWindow" x:DataType="viewModels:MainWindowViewModel"
|
||||
Title="Main Window" WindowStartupLocation="CenterScreen" WindowState="Maximized" >
|
||||
Title="Main Window" WindowStartupLocation="CenterScreen" WindowState="Maximized">
|
||||
<SplitView IsPaneOpen="True"
|
||||
DisplayMode="Inline"
|
||||
PanePlacement="Right">
|
||||
@@ -15,7 +15,7 @@
|
||||
Content="{Binding SidebarContent}" />
|
||||
<TextBox Grid.Row="1" Margin="5" Watermark="Notes..."
|
||||
Text="{Binding SelectedCoin.Notes}"
|
||||
IsReadOnly="False" TextWrapping="Wrap"/>
|
||||
IsReadOnly="False" TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</SplitView.Pane>
|
||||
<Grid RowDefinitions="*, Auto" ShowGridLines="False">
|
||||
@@ -51,7 +51,7 @@
|
||||
Binding="{Binding PriceGuideValue, StringFormat='${0:0.00}'}"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn Header="PCGS #" Width="*"
|
||||
Binding="{Binding PCGSNo}"
|
||||
Binding="{Binding PcgsNumber}"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn Header="Quantity" Width="*"
|
||||
Binding="{Binding Quantity}"
|
||||
@@ -77,4 +77,4 @@
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</SplitView>
|
||||
</Window>
|
||||
</Window>
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reactive;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Platform.Storage;
|
||||
@@ -36,6 +35,7 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel> {
|
||||
|
||||
private async Task ShowFilePickerAsync(InteractionContext<Unit, Uri> interationContext) {
|
||||
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 {
|
||||
Title = "Save CSV...",
|
||||
DefaultExtension = ".csv",
|
||||
@@ -44,15 +44,13 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel> {
|
||||
SuggestedStartLocation = await StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
|
||||
});
|
||||
|
||||
if (file is not null) {
|
||||
if (file is not null)
|
||||
interationContext.SetOutput(file.Path);
|
||||
}
|
||||
else {
|
||||
else
|
||||
// TODO Handle user closing the dialog.
|
||||
throw new Exception("User cancelled the file picker.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task ShowErrorWindowAsync(InteractionContext<ErrorWindowViewModel, bool> interaction) {
|
||||
var window = new ErrorWindow();
|
||||
window.DataContext = interaction.Input;
|
||||
@@ -60,4 +58,4 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel> {
|
||||
var result = await window.ShowDialog<bool>(this);
|
||||
interaction.SetOutput(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Window 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"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="PcgsInvUi.Views.MainWindowRewrite"
|
||||
Title="MainWindowRewrite">
|
||||
|
||||
<Grid>
|
||||
<TextBlock Text="This is a testing block!" />
|
||||
</Grid>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace PcgsInvUi.Views;
|
||||
|
||||
public partial class MainWindowRewrite : Window {
|
||||
public MainWindowRewrite() {
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace PcgsInvUi.Views;
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# pcgs-inv
|
||||
A coin collection inventory app, written in C# with the Avalonia UI framework utilizing the PCGS Public API.
|
||||
|
||||
# Requirements
|
||||
[.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
|
||||
Extract contents of the downloaded file to an accessible folder (ie. Documents/PCGSApp)
|
||||
|
||||
As of right now, there's no fancy installer to be used - shortcuts will have to be made by you.
|
||||
|
||||
# Usage
|
||||
Usage is limited as of now, however input, deletion, and export of information is working and should not change much
|
||||
over the development of the app.
|
||||
|
||||
To input a coin, you will need to know the grade and PCGS id (no NGS support, and is not planned yet) of the coin you
|
||||
wish to input. You can put them into the sidebar to run a request to the PCGS Public API, at which point you will need
|
||||
to be patient, due to the fact that the API is quite slow.
|
||||
|
||||
There is note functionality - with a coin selected, you may use the Notes section on the sidebar in order to input
|
||||
that information. Multiline notes are not currently supported, but they are planned for the future.
|
||||
|
||||
To change the quantity of a specific coin, click on the corresponding value for the displayed coin. This will allow you
|
||||
to type in a new value for the quantity. The total value of that row and the total value of the collection (displayed
|
||||
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
|
||||
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
|
||||
@@ -2,6 +2,10 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PcgsInvUi", "PcgsInvUi\PcgsInvUi.csproj", "{887D7BD0-3E71-435D-82CD-3DAF3187D056}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PcgsInv", "PcgsInv\PcgsInv.csproj", "{8B62E70B-9F3C-4E1B-9E7D-FBADC680E5D0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PcgsApi", "PcgsApi\PcgsApi.csproj", "{08B02A6D-EEA5-4335-AC5B-B1347CEBBE88}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -13,5 +17,13 @@ Global
|
||||
{887D7BD0-3E71-435D-82CD-3DAF3187D056}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{887D7BD0-3E71-435D-82CD-3DAF3187D056}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{887D7BD0-3E71-435D-82CD-3DAF3187D056}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{8B62E70B-9F3C-4E1B-9E7D-FBADC680E5D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8B62E70B-9F3C-4E1B-9E7D-FBADC680E5D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8B62E70B-9F3C-4E1B-9E7D-FBADC680E5D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B62E70B-9F3C-4E1B-9E7D-FBADC680E5D0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08B02A6D-EEA5-4335-AC5B-B1347CEBBE88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08B02A6D-EEA5-4335-AC5B-B1347CEBBE88}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08B02A6D-EEA5-4335-AC5B-B1347CEBBE88}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08B02A6D-EEA5-4335-AC5B-B1347CEBBE88}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -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