Author SHA1 Message Date
alphafetus 51f1eb8c65 chore: commit missing files 2026-06-21 00:47:18 -04:00
alphafetus 3a819ce180 temp: broken push to laptop 2026-06-19 12:31:12 -04:00
alphafetus 0a0ea86d25 Changed version number to conform to dotnet rules. 2024-01-08 14:17:59 -05:00
alphafetus 33472c727d Incremented version number. 2024-01-08 14:16:53 -05:00
Lucas Vas a04cce4817 Merge pull request from BobdaFett/api-key-management
Add API key management.
2024-01-08 14:07:18 -05:00
alphafetus 5e516fc333 Removed unnecessary files. 2024-01-08 14:06:50 -05:00
alphafetus 0ff7edfed6 Refactored some code. 2024-01-08 14:02:58 -05:00
Lucas Vas af4f7567b4 Update README.md 2024-01-08 14:01:39 -05:00
alphafetus ba04d31c3e Added comments to CoinCollection code, and finished a basic framework for API key entry. 2023-12-01 23:15:27 -05:00
alphafetus 683a2cf87b Once again pushing broken code 2023-11-30 16:56:24 -05:00
alphafetus fab011b898 I made it work 2023-11-30 13:35:25 -05:00
alphafetus 2b4ed5374a Update README.md again. 2023-11-30 00:29:59 -05:00
alphafetus 7b3af8f7f1 Updated README.md 2023-11-30 00:28:27 -05:00
alphafetus c0b884fa1d Added a makefile - on windows run with nmake <option> 2023-11-29 21:54:13 -05:00
alphafetus 88d209ab83 Merged broken code. 2023-11-29 21:32:16 -05:00
alphafetus 7eb096767c Pushing more possibly broken code. 2023-11-29 21:31:36 -05:00
alphafetus 4b2504b48c Pushing currently broken code. 2023-11-29 21:29:47 -05:00
alphafetus 435d01ef90 Started framework of API key storage. 2023-11-25 02:16:53 -05:00
Lucas Vas e21ed50dc5 Delete .idea/.idea.pcgs-inv/.idea directory
Deleted gitignored file.
2023-11-25 02:01:19 -05:00
alphafetus a0da899537 Merge branch 'master' of https://github.com/BobdaFett/pcgs-inv 2023-11-23 19:08:18 -05:00
Lucas Vas 58a3bc1c7d Merge pull request #4 from BobdaFett/BobdaFett-patch-1
Update README.md
2023-11-23 19:07:09 -05:00
Lucas Vas 5433ee6719 Update README.md 2023-11-23 19:06:46 -05:00
Lucas Vas f0849cb1f9 Create README.md 2023-11-23 19:05:38 -05:00
alphafetus e17d467fb1 Merge branch 'vas-working' of https://github.com/BobdaFett/pcgs-inv into vas-working 2023-11-23 18:20:20 -05:00
alphafetus 1145f3ef5e Fixed totaling of CoinCollection's value. 2023-11-23 18:19:47 -05:00
alphafetus 71673206db Allowed ErrorWindow to close properly. 2023-11-23 18:19:47 -05:00
alphafetus f98ff942bb Fixed DeleteWindow and allowed it to close. 2023-11-23 18:19:47 -05:00
alphafetus 83a08edb2e Added code ensuring that there is not a crash upon creation of sqlite database. 2023-11-23 18:19:47 -05:00
alphafetus 602e9bbd8b Slapped bandaids on things for a fast release test. 2023-11-23 18:19:47 -05:00
alphafetus e73f9db02c ShowErrorWindow now actually shows the ErrorWindow 2023-11-23 18:19:47 -05:00
alphafetus d667e86b1b Added most of the framework for the error window. 2023-11-23 18:19:47 -05:00
alphafetus bc8f5e4d54 idek tbh 2023-11-23 18:19:47 -05:00
Lucas Vas 7d99a9a07e Merge pull request #3 from BobdaFett/vas-working
Fixed issues relating to proper creation and recording of coin collection totals, along with proper modal dialog functionality.
These are my own changes, so I will override requirements.
2023-11-23 18:18:22 -05:00
alphafetus 24a9a2b71e Fixed totaling of CoinCollection's value. 2023-11-23 18:13:34 -05:00
alphafetus 93219aaf9b Allowed ErrorWindow to close properly. 2023-11-23 17:35:40 -05:00
alphafetus 663fb256e5 Fixed DeleteWindow and allowed it to close. 2023-11-23 16:47:55 -05:00
Lucas Vas cfd15ad310 Merge pull request #2 from BobdaFett/vas-working
Bring master up to date with working directory.
2023-11-22 19:05:08 -05:00
alphafetus 41667d9e0b Added code ensuring that there is not a crash upon creation of sqlite database. 2023-11-22 18:29:21 -05:00
alphafetus 9c6ea08221 Slapped bandaids on things for a fast release test. 2023-11-22 14:39:17 -05:00
alphafetus 4fcae0b3c7 ShowErrorWindow now actually shows the ErrorWindow 2023-11-15 11:34:02 -05:00
alphafetus 99a666afd8 Added most of the framework for the error window. 2023-11-14 19:07:11 +00:00
alphafetus 602d27d45f Merge remote-tracking branch 'origin/vas-working' into vas-working 2023-11-14 17:54:44 +00:00
alphafetus b1344b174c Changed .NET target to 6.0 2023-11-14 17:54:35 +00:00
alphafetus d0a2fefbbf idek tbh 2023-11-14 12:50:07 -05:00
alphafetus f2fced1b44 Allowed coins to be checked before sending API requests. 2023-11-09 13:30:54 -05:00
alphafetus 937832bb26 Removed FindCommand until functionality is needed. 2023-10-25 12:56:20 -04:00
alphafetus 14bb30bee8 Fixed crash with FilePicker when cancelled. 2023-10-25 12:54:48 -04:00
alphafetus ddbc745200 Implemented export to CSV functionality. 2023-10-25 08:45:01 -04:00
alphafetus a478d4ebb0 Integrated SQLite database. 2023-10-24 12:08:30 -04:00
alphafetus 39bcdb3970 Commit before possibly breaking code. 2023-10-24 10:46:25 -04:00
alphafetus 99cb9268dd Reformatted and cleaned up code across whole project. 2023-10-24 09:04:04 -04:00
alphafetus 42a7871ed4 Allowed FindWindow to display through MainWindow. 2023-10-18 17:19:02 -04:00
alphafetus 0b438fb49e Added FindCommand as a binding. 2023-10-18 17:14:08 -04:00
alphafetus 3234b12184 Created frame for FindWindow, shortened CoinCollection 2023-10-18 17:12:49 -04:00
alphafetus f298296471 Moved the TODO list and removed an unnecessary comment. 2023-10-18 16:13:12 -04:00
alphafetus c97db12f8a Disabled DeleteButton when no coin is selected. 2023-10-18 14:17:57 -04:00
alphafetus 298e6d8294 Updated TODO lists, deleted unused .axaml 2023-10-18 14:09:36 -04:00
alphafetus 8f489775ef Excluded /obj/ from .csproj file. 2023-10-18 13:24:56 -04:00
alphafetus c798fab588 Updated .gitignore and deleted /bin/ 2023-10-18 13:23:29 -04:00
alphafetus 312f5f6b72 Updated .gitignore and deleted /obj/ 2023-10-18 13:21:04 -04:00
alphafetus 44d7e7842f Edited .gitignore 2023-10-18 13:16:39 -04:00
alphafetus 37a6d24049 Altered CancelCommand. 2023-10-18 13:12:36 -04:00
alphafetus ba7e12e51b Updated .gitignore 2023-10-17 14:27:16 -04:00
alphafetus 8249d9a27b Added .gitignore and created delete window. 2023-10-17 14:25:14 -04:00
alphafetus 8c53024d5c Added newControl and fixed DataGrid. 2023-10-10 09:17:28 -04:00
212 changed files with 1663 additions and 7579 deletions

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
/PcgsInvUi/[Bb]in/
/PcgsInvUi/[Oo]bj/
[Oo]bj/
[Bb]in/
ALL_BUILD.vcxproj
ALL_BUILD.vcxproj.filters
ZERO_CHECK.vcxproj
ZERO_CHECK.vcxproj.filters
CMakeCache.txt
CMakeFiles/
.idea/
cmake_install.cmake
coin_collection
-13
View File
@@ -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
-16
View File
@@ -1,16 +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/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>
-7
View File
@@ -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>
-4
View File
@@ -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
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>
+5
View File
@@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.25)
project("PcgsInvUi" CSharp)
add_subdirectory(PcgsInvUi)
+17
View File
@@ -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>
+127
View File
@@ -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) { }
}
+38
View File
@@ -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));
}
}
+15
View File
@@ -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>
+22
View File
@@ -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

+47
View File
@@ -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);
}
}
+39
View File
@@ -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>
+27
View File
@@ -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();
}
+31
View File
@@ -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;
}
}
+30
View File
@@ -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());
}
}
+5
View File
@@ -0,0 +1,5 @@
using ReactiveUI;
namespace PcgsInv.ViewModels;
public abstract class ViewModelBase : ReactiveObject { }
+64
View File
@@ -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>
+9
View File
@@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace PcgsInv.Views;
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
}
+18
View File
@@ -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>
+3 -3
View File
@@ -1,12 +1,12 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PcgsInvUi.App"
xmlns:local="using:PcgsInvUi"
RequestedThemeVariant="Dark">
xmlns:pcgsInvUi="clr-namespace:PcgsInvUi"
RequestedThemeVariant="Light">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
<pcgsInvUi:ViewLocator />
</Application.DataTemplates>
<Application.Styles>
+11 -14
View File
@@ -1,30 +1,27 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using PcgsInvUi.Services;
using PcgsInvUi.ViewModels;
using PcgsInvUi.Views;
namespace PcgsInvUi;
public partial class App : Application
{
public override void Initialize()
{
public class App : Application {
public override void Initialize() {
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
public override void OnFrameworkInitializationCompleted() {
base.OnFrameworkInitializationCompleted();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var coins = new Services.CoinCollection();
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(coins),
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
var coins = new CoinDatabase();
desktop.MainWindow = new MainWindow {
DataContext = new MainWindowViewModel(coins)
};
// Allow the application to save the database when it exits.
desktop.Exit += (sender, args) => { coins.UpdateCollection("CollectionTable"); };
}
base.OnFrameworkInitializationCompleted();
+47
View File
@@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.25)
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
)
# Set the .NET Framework version for the executable.
set_property(TARGET PcgsInvUi PROPERTY VS_DOTNET_TARGET_FRAMEWORK_VERSION "v4.7.2")
# Set the executable to be 64-bit.
set_property(TARGET PcgsInvUi PROPERTY WIN64_EXECUTABLE TRUE)
# Set the C# language version (defaults to 3.0)
set(CMAKE_CSharp_FLAGS "/langversion:latest")
# Add the .NET reference libraries as dependencies.
set_property(TARGET PcgsInvUi PROPERTY VS_DOTNET_REFERENCES
"Microsoft.CSharp"
)
-135
View File
@@ -1,135 +0,0 @@
using System;
using ReactiveUI;
namespace PcgsInvUi.Models;
public class Coin : ReactiveObject
{
public int PcgsNumber
{
get => _pcgsNumber;
set => this.RaiseAndSetIfChanged(ref _pcgsNumber, value);
}
private int _pcgsNumber;
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 => 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 Series
{
get => _series;
set => this.RaiseAndSetIfChanged(ref _series, value);
}
private string _series;
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 => this.RaiseAndSetIfChanged(ref _quantity, value);
}
private int _quantity;
public double TotalPrice => Quantity * PriceGuideValue;
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;
}
+85 -9
View File
@@ -1,26 +1,102 @@
<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>
<AvaloniaResource Include="Assets\**" />
<AvaloniaResource Include="Assets\**"/>
<AvaloniaResource Remove="obj\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.2" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.2" />
<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.2" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<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="2.0.3"/>
</ItemGroup>
<ItemGroup>
<Content Include="obj\Debug\net6.0\apphost.exe"/>
<Content Include="obj\Debug\net6.0\Avalonia\original.dll"/>
<Content Include="obj\Debug\net6.0\Avalonia\original.pdb"/>
<Content Include="obj\Debug\net6.0\Avalonia\original.ref.dll"/>
<Content Include="obj\Debug\net6.0\Avalonia\references"/>
<Content Include="obj\Debug\net6.0\Avalonia\resources"/>
<Content Include="obj\Debug\net6.0\Avalonia\Resources.Inputs.cache"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.AssemblyInfoInputs.cache"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.assets.cache"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.csproj.AssemblyReference.cache"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.csproj.CopyComplete"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.csproj.CoreCompileInputs.cache"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.csproj.FileListAbsolute.txt"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.dll"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.GeneratedMSBuildEditorConfig.editorconfig"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.genruntimeconfig.cache"/>
<Content Include="obj\Debug\net6.0\PcgsInvUi.pdb"/>
<Content Include="obj\Debug\net6.0\refint\PcgsInvUi.dll"/>
<Content Include="obj\Debug\net6.0\ref\PcgsInvUi.dll"/>
<Content Include="obj\PcgsInvUi.csproj.nuget.dgspec.json"/>
<Content Include="obj\PcgsInvUi.csproj.nuget.g.props"/>
<Content Include="obj\PcgsInvUi.csproj.nuget.g.targets"/>
<Content Include="obj\project.assets.json"/>
<Content Include="obj\project.nuget.cache"/>
<Content Include="obj\project.packagespec.json"/>
<Content Include="obj\rider.project.model.nuget.info"/>
<Content Include="obj\rider.project.restore.info"/>
</ItemGroup>
<ItemGroup>
<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"/>
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="obj\**"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="obj\**"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Remove="obj\**"/>
</ItemGroup>
<ItemGroup>
<None Remove="obj\**"/>
</ItemGroup>
<ItemGroup>
<Reference Include="System.Data"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Models\"/>
</ItemGroup>
</Project>
+9 -8
View File
@@ -1,23 +1,24 @@
using Avalonia;
using System;
using Avalonia;
using Avalonia.ReactiveUI;
using System;
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();
}
}
-14
View File
@@ -1,14 +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(),
};
}
+444
View File
@@ -0,0 +1,444 @@
using System;
using System.Collections.ObjectModel;
using System.Data.SQLite;
using System.IO;
using System.Threading.Tasks;
namespace PcgsInvUi.Services;
public class CoinDatabase {
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 base table.
CreateCollection("CollectionTable");
CreateApiTable();
Console.WriteLine("Connected to database.");
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.
UpdateCollection("CollectionTable");
// Close the connection.
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 (!File.Exists("coin_collection")) {
// If not, create the database.
Console.Write("Creating database... ");
SQLiteConnection.CreateFile("coin_collection.sqlite");
Console.WriteLine("Done.");
}
// Create a connection to the database.
var connection =
new SQLiteConnection("DataSource=coin_collection");
// Open the connection.
try {
connection.Open();
IsConnected = true;
}
catch (Exception e) {
Console.WriteLine(e.Message);
}
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) {
// 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 {collectionName} (" +
"PCGS_NUMBER TEXT," +
"GRADE TEXT" +
"NAME TEXT," +
"YEAR INTEGER," +
"DENOMINATION TEXT," +
"MINT_MARK TEXT," +
"PRICE_GUIDE_VALUE REAL," +
"COIN_FACTS_LINK TEXT," +
"MAJOR_VARIETY TEXT," +
"MINOR_VARIETY TEXT," +
"DIE_VARIETY TEXT," +
"SERIES_NAME TEXT," +
"CATEGORY TEXT," +
"DESIGNATION TEXT," +
"CERT_NUM INTEGER," +
"QUANTITY INTEGER," +
"NOTES TEXT," +
"PRIMARY KEY(PCGS_NUMBER, GRADE))";
// Execute the command.
cmd.ExecuteNonQuery();
Console.WriteLine("Collection table initialized.");
}
/// <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) {
// TODO Create a NotConnectedToDatabaseException
Console.WriteLine("Not connected to database.");
return;
}
// Create a SQL command.
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 ('{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.
// First, check if the coin already exists - if it does, just update the quantity by adding the amount entered.
var coin = GetCoin(pcgsNumber, grade);
if (coin is not null) {
coin.Quantity += quantity;
SaveCoin("CollectionTable", coin);
return PcgsClient.ErrorType.None;
}
// Otherwise, build a new coin with the API and insert it.
var apiResult = await _pcgsClient.GetCoinFactsByGrade(pcgsNumber, grade);
if (apiResult.Item1 == PcgsClient.ErrorType.None) {
var newCoin = apiResult.Item2;
newCoin.Quantity = quantity;
InsertCoin(newCoin);
Collection.Add(newCoin);
return PcgsClient.ErrorType.None;
}
Console.WriteLine("Error getting coin from API - {0}", apiResult.Item1);
return apiResult.Item1;
}
/// <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}...");
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.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.
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...");
var cmd = Connection.CreateCommand();
cmd.CommandText = $"SELECT * FROM {collectionName}";
using (var reader = cmd.ExecuteReader()) {
while (reader.Read())
// Create coin from reader then add to list.
Collection.Add(CoinFromReader(reader));
}
}
/// <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()) {
resultString += reader.GetString(1) + ",";
numHeaders++;
}
}
resultString += "\n";
// Is there a more efficient way of doing this? This is running at O(n^2) time.
using (var reader = cmd.ExecuteReader()) {
while (reader.Read()) {
// Read information from the reader, then add to CSV string.
for (var i = 0; i < numHeaders; i++) {
// Get the type of each column.
var type = reader.GetFieldType(i);
Console.WriteLine("Type: " + type);
if (type == typeof(long))
resultString += reader.GetInt32(i) + ",";
else if (type == typeof(double))
resultString += reader.GetDouble(i) + ",";
else if (type == typeof(string))
resultString += reader.GetString(i) + ",";
else
resultString += "NULL" + ",";
}
resultString += "\n";
}
}
return resultString;
}
/// <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...");
var cmd = Connection.CreateCommand();
cmd.CommandText = $"UPDATE {collectionName} " +
$"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.
}
/// <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.
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;
}
/// <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", 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.") { }
}
-6
View File
@@ -1,6 +0,0 @@
namespace PcgsInvUi.Services;
public class Database
{
// TODO Create a database using SQLite.
}
-51
View File
@@ -1,51 +0,0 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json;
using PcgsInvUi.Models;
namespace PcgsInvUi.Services;
public class PcgsClient
{
private string _apiUri = "https://api.pcgs.com/publicapi";
private readonly string _apiKey;
public PcgsClient(string apiKey)
{
_apiKey = apiKey;
}
public async Task<Coin> GetCoinFactsByGrade(int pcgsNumber, int grade, bool plusGrade)
{
Coin newCoin = null;
using (HttpClient client = new HttpClient())
{
string requestUri =
$"{_apiUri}/coindetail/GetCoinFactsByGrade/?PCGSNo={pcgsNumber}&GradeNo={grade}&PlusGrade={plusGrade}";
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", _apiKey);
HttpResponseMessage response = await client.GetAsync(requestUri);
if (response.IsSuccessStatusCode)
{
string jsonResponse = await response.Content.ReadAsStringAsync();
newCoin = ParseJsonResponse(jsonResponse);
}
else
{
Console.WriteLine($"Error: {response.StatusCode} - {response.ReasonPhrase}");
}
}
return newCoin;
}
// TODO Will eventually create the other methods that are included in the Public API.
private Coin ParseJsonResponse(string json)
{
Coin newCoin = JsonConvert.DeserializeObject<Coin>(json);
return newCoin;
}
}
+4 -10
View File
@@ -5,23 +5,17 @@ using PcgsInvUi.ViewModels;
namespace PcgsInvUi;
public class ViewLocator : IDataTemplate
{
public Control Build(object data)
{
public class ViewLocator : IDataTemplate {
public Control Build(object data) {
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
if (type != null) return (Control)Activator.CreateInstance(type)!;
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object data)
{
public bool Match(object data) {
return data is ViewModelBase;
}
}
+18
View File
@@ -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");
}
}
+12 -16
View File
@@ -1,24 +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; }
}
@@ -0,0 +1,14 @@
using System.Reactive;
using ReactiveUI;
namespace PcgsInvUi.ViewModels;
public class DeleteWindowViewModel : ViewModelBase {
public DeleteWindowViewModel() {
OkCommand = ReactiveCommand.Create(() => (object)true);
CancelCommand = ReactiveCommand.Create(() => (object)false);
}
public ReactiveCommand<Unit, object> OkCommand { get; }
public ReactiveCommand<Unit, object> CancelCommand { get; }
}
+13 -17
View File
@@ -1,30 +1,26 @@
using System;
using System.Reactive;
using PcgsInvUi.Models;
using System.Reactive;
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 class EditViewModel : ViewModelBase {
public EditViewModel(PcgsCoin pcgsCoin) {
CurrentPcgsCoin = pcgsCoin;
var okEnabled = this.WhenAnyValue(
x => x.CurrentCoin.PcgsNumber,
x => x.CurrentCoin.Grade,
x => x.CurrentCoin.Quantity,
(id, grade, quantity) => (id != 0) &&
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; }
}
@@ -0,0 +1,22 @@
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace PcgsInvUi.ViewModels;
public partial class ErrorWindowViewModel : ViewModelBase {
private readonly Window _dialog;
public ErrorWindowViewModel(Window dialog, string message) {
_dialog = dialog;
Message = message;
}
[ObservableProperty]
public partial string Message { get; set; }
[RelayCommand]
public void Close() {
_dialog.Close();
}
}
@@ -0,0 +1,8 @@
using System.Reactive;
using ReactiveUI;
namespace PcgsInvUi.ViewModels;
public class FindWindowViewModel : ViewModelBase {
public ReactiveCommand<Unit, object> OkCommand { get; } = ReactiveCommand.Create(() => (object)1);
}
+119 -45
View File
@@ -1,66 +1,140 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
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;
public class MainWindowViewModel : ViewModelBase
{
public CoinListViewModel List { get; }
private ViewModelBase _mainContent;
private ViewModelBase _sideContent;
public MainWindowViewModel(CoinCollection coins)
{
List = new CoinListViewModel(coins.GetItems());
MainContent = List;
}
public ViewModelBase MainContent
{
get => _mainContent;
private set => this.RaiseAndSetIfChanged(ref _mainContent, value);
}
public ViewModelBase SidebarContent
{
get => _sideContent;
private set => this.RaiseAndSetIfChanged(ref _sideContent, value);
}
public void AddItem()
{
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();
var apiKeyViewModel = new ApiKeyViewModel();
// DisplayedList = CoinCollection = new ObservableCollection<Coin>(coins.GetItems());
DisplayedList = ConnectedCoinDatabase.Collection;
TotalValue = DisplayedList.Sum(x => x.TotalPrice);
Observable.Merge(
newViewModel.OkCommand,
newViewModel.CancelCommand.Select(_ => (Coin?)null))
.Take(1)
.Subscribe(model =>
{
if (model != null)
List.CoinCollection.Add(model);
MainContent = List;
// 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.
});
MainContent = newViewModel;
// 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;
}
public void EditItem()
{
var editViewModel = new EditViewModel(List.SelectedCoin);
// 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)
.Select(x => x != null);
DeleteCommand = ReactiveCommand.CreateFromTask(async () => {
var deleteViewModel = new DeleteWindowViewModel();
var result = await ShowDeleteWindow.Handle(deleteViewModel);
if (result && SelectedCoin is not null) {
ConnectedCoinDatabase.DeleteCoin(SelectedCoin);
ConnectedCoinDatabase.Collection.Remove(SelectedCoin);
}
}, deleteEnabled);
ShowExportWindow = new Interaction<Unit, Uri>();
ExportCommand = ReactiveCommand.CreateFromTask(async () => {
try {
var filePath = await ShowExportWindow.Handle(Unit.Default);
var stream = File.Open(filePath.AbsolutePath, FileMode.OpenOrCreate);
var writer = new StreamWriter(stream);
await writer.WriteAsync(ConnectedCoinDatabase.CollectionToCSV("CollectionTable"));
await writer.FlushAsync();
}
catch (IOException e) {
// Something is wrong with the file after it's been created.
Console.WriteLine(e.Message);
await ShowErrorWindow.Handle(new ErrorWindowViewModel(e.Message));
}
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."));
break;
case PcgsClient.ErrorType.InvalidRequestFormat:
Console.WriteLine("Request format is invalid - showing window.");
await ShowErrorWindow.Handle(new ErrorWindowViewModel("Request format is invalid."));
break;
case PcgsClient.ErrorType.NoCoinFound:
Console.WriteLine("No coin was found with the given parameters.");
await ShowErrorWindow.Handle(
new ErrorWindowViewModel("No coin was found with the given parameters."));
break;
}
public void DeleteItem()
{
// 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)
.Subscribe(_ => TotalValue = ConnectedCoinDatabase.Collection.Sum(x => x.TotalPrice));
// Update TotalValue anytime a coin is added or removed.
ConnectedCoinDatabase.Collection.CollectionChanged += (sender, args) => {
TotalValue = ConnectedCoinDatabase.Collection.Sum(x => x.TotalPrice);
};
}
public void FindItem()
{
[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");
}
}
+36 -36
View File
@@ -1,36 +1,15 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Collections.Generic;
using PcgsInvUi.Models;
using ReactiveUI;
namespace PcgsInvUi.ViewModels;
public class NewViewModel : ViewModelBase
{
private string _pcgsNumber;
private string _grade;
private string _quantity;
public class NewViewModel : ViewModelBase {
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()
{
public NewViewModel() {
var okEnabled = this.WhenAnyValue(
x => x.PcgsNumber,
x => x.Grade,
@@ -39,19 +18,15 @@ public class NewViewModel : ViewModelBase
!string.IsNullOrWhiteSpace(grade) &&
!string.IsNullOrWhiteSpace(quantity));
// This will eventually send our request to the public API, then construct our coin.
OkCommand = ReactiveCommand.Create(
() => new Coin { PcgsNumber = Int32.Parse(PcgsNumber),
Grade = Grade,
Quantity = Int32.Parse(Quantity) },
() => (int.Parse(PcgsNumber), Grade, int.Parse(Quantity)),
okEnabled);
CancelCommand = ReactiveCommand.Create(() => { });
ClearCommand = ReactiveCommand.Create(() => { 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.
GradesList = new List<string>()
{
GradesList = new List<string> {
"P-01",
"FR-02",
"AG-03",
@@ -85,7 +60,32 @@ public class NewViewModel : ViewModelBase
};
}
public ReactiveCommand<Unit, Coin> OkCommand { get; }
public ReactiveCommand<Unit, Unit> CancelCommand { get; }
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 = "";
}
}
+2 -4
View File
@@ -1,7 +1,5 @@
using ReactiveUI;
using CommunityToolkit.Mvvm.ComponentModel;
namespace PcgsInvUi.ViewModels;
public class ViewModelBase : ReactiveObject
{
}
public class ViewModelBase : ObservableObject { }
+14
View File
@@ -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>
+9
View File
@@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace PcgsInvUi.Views;
public partial class ApiKeyView : UserControl {
public ApiKeyView() {
InitializeComponent();
}
}
-73
View File
@@ -1,73 +0,0 @@
<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.CoinListView"
x:DataType="viewModels:CoinListViewModel">
<DockPanel>
<DockPanel DockPanel.Dock="Top">
<DataGrid x:Name="CollectionView"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
DockPanel.Dock="Top"
ItemsSource="{Binding CoinCollection}"
SelectedItem="{Binding SelectedCoin, Mode=TwoWay}"
AutoGenerateColumns="False"
CanUserResizeColumns="True" >
<DataGrid.Columns>
<DataGridTextColumn Header="Series" Width="*"
Binding="{Binding Series}"
IsReadOnly="True"/>
<DataGridTextColumn Header="Year"
Binding="{Binding Year}"
IsReadOnly="True"/>
<DataGridTextColumn Header="Mint" Width="*"
Binding="{Binding MintMark}"
IsReadOnly="True"/>
<DataGridTextColumn Header="Denomination" Width="*"
Binding="{Binding Denomination}"
IsReadOnly="True"/>
<DataGridTextColumn Header="Variety" Width="*"
Binding="{Binding MajorVariety}"
IsReadOnly="True"/>
<DataGridTextColumn Header="Grade" Width="*"
Binding="{Binding Grade}"
IsReadOnly="True"/>
<DataGridTextColumn Header="Designation" Width="*"
Binding="{Binding Designation}"
IsReadOnly="True"/>
<DataGridTextColumn Header="Est. Price" Width="*"
Binding="{Binding PriceGuideValue}"
IsReadOnly="False"/>
<DataGridTextColumn Header="PCGS #" Width="*"
Binding="{Binding PcgsNumber}"
IsReadOnly="True"/>
<DataGridTextColumn Header="Quantity" Width="*"
Binding="{Binding Quantity}"
IsReadOnly="False"/>
<DataGridTextColumn Header="Total Price" Width="*"
Binding="{Binding TotalPrice}"
IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
<DockPanel DockPanel.Dock="Bottom">
<Grid HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="5" ColumnDefinitions="Auto, Auto, Auto, Auto" DockPanel.Dock="Left">
<Button Grid.Column="0" Margin="5" Width="100" HorizontalContentAlignment="Center" Content="New..."
Command="{Binding $parent[Window].((viewModels:MainWindowViewModel)DataContext).AddItem}"/>
<Button Grid.Column="1" Margin="5" Width="100" HorizontalContentAlignment="Center" Content="Edit..."
Command="{Binding $parent[Window].((viewModels:MainWindowViewModel)DataContext).EditItem}"/>
<Button Grid.Column="2" Margin="5" Width="100" HorizontalContentAlignment="Center" Content="Delete"
Command="{Binding $parent[Window].((viewModels:MainWindowViewModel)DataContext).DeleteItem}"/>
<Button Grid.Column="3" Margin="5" Width="100" HorizontalContentAlignment="Center" Content="Find..."
Command="{Binding $parent[Window].((viewModels:MainWindowViewModel)DataContext).FindItem}"/>
</Grid>
<Grid HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="5" ColumnDefinitions="Auto, Auto" DockPanel.Dock="Right">
<Label Grid.Column="0" Margin="5" VerticalAlignment="Center" Content="{Binding TotalValue, StringFormat='Total Value: ${0:0.00}'}" />
<Button Grid.Column="1" Margin="5" Width="150" HorizontalContentAlignment="Center" Content="Export to CSV..." />
</Grid>
</DockPanel>
</DockPanel>
</UserControl>
-13
View File
@@ -1,13 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace PcgsInvUi.Views;
public partial class CoinListView : UserControl
{
public CoinListView()
{
InitializeComponent();
}
}
+6 -6
View File
@@ -1,24 +1,24 @@
<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"
xmlns:viewModels="clr-namespace:PcgsInvUi.ViewModels"
d:DesignWidth="325" d:DesignHeight="100"
x:Class="PcgsInvUi.Views.DeleteWindow"
x:Class="PcgsInvUi.Views.DeleteWindow" x:DataType="viewModels:DeleteWindowViewModel"
Width="325" Height="100" WindowStartupLocation="CenterOwner"
Title="DeleteWindow">
<Grid ColumnDefinitions="*, *" RowDefinitions="*, *">
<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="10"
HorizontalAlignment="Center" VerticalAlignment="Center"
Content="Are you sure you want to delete this item?"/>
Content="Are you sure you want to delete this item?" />
<Button Grid.Row="1" Grid.Column="0" Margin="5"
Width="100"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
HorizontalContentAlignment="Center"
Content="Yes" Click="AcceptClicked"/>
Content="Yes" Command="{Binding OkCommand}" />
<Button Grid.Row="1" Grid.Column="1" Margin="5"
Width="100"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
HorizontalContentAlignment="Center"
Content="No" Click="CancelClicked"/>
HorizontalContentAlignment="Center" IsEnabled="True"
Content="No" Command="{Binding CancelCommand}" />
</Grid>
</Window>
+8 -21
View File
@@ -1,27 +1,14 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;
using Avalonia.ReactiveUI;
using PcgsInvUi.ViewModels;
using ReactiveUI;
namespace PcgsInvUi.Views;
public partial class DeleteWindow : Window
{
public bool Accepted { get; set; }
public DeleteWindow()
{
public partial class DeleteWindow : ReactiveWindow<DeleteWindowViewModel> {
public DeleteWindow() {
InitializeComponent();
Accepted = false;
}
public void AcceptClicked(object source, RoutedEventArgs args)
{
// TODO Delete the SelectedCoin?
Accepted = true;
Close();
}
public void CancelClicked(object source, RoutedEventArgs args)
{
Close();
this.WhenActivated(d => d(ViewModel!.OkCommand.Subscribe(Close)));
this.WhenActivated(d => d(ViewModel!.CancelCommand.Subscribe(Close)));
}
}
+18
View File
@@ -0,0 +1,18 @@
<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"
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">
<Grid RowDefinitions="*,*">
<Label Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Stretch"
Margin="15"
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>
+11
View File
@@ -0,0 +1,11 @@
using Avalonia.ReactiveUI;
using PcgsInvUi.ViewModels;
namespace PcgsInvUi.Views;
public partial class ErrorWindow : ReactiveWindow<ErrorWindowViewModel> {
public ErrorWindow() {
InitializeComponent();
this.WhenActivated(d => d(ViewModel!.CloseCommand.Subscribe(Close)));
}
}
+14
View File
@@ -0,0 +1,14 @@
<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.FindWindow"
Title="FindWindow">
<Grid ColumnDefinitions="Auto, *" RowDefinitions="*, *">
<Label Content="PCGS #:" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5" />
<TextBox Grid.Row="0" Grid.Column="1" Margin="5" />
<Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Content="Find"
HorizontalAlignment="Center" Margin="5" Width="75" />
</Grid>
</Window>
+13
View File
@@ -0,0 +1,13 @@
using System;
using Avalonia.ReactiveUI;
using PcgsInvUi.ViewModels;
using ReactiveUI;
namespace PcgsInvUi.Views;
public partial class FindWindow : ReactiveWindow<FindWindowViewModel> {
public FindWindow() {
InitializeComponent();
this.WhenActivated(d => d(ViewModel!.OkCommand.Subscribe(Close)));
}
}
+69 -4
View File
@@ -5,11 +5,76 @@
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="MainWindow">
<SplitView Name="Sidebar">
Title="Main Window" WindowStartupLocation="CenterScreen" WindowState="Maximized">
<SplitView IsPaneOpen="True"
DisplayMode="Inline"
PanePlacement="Right">
<SplitView.Pane>
<UserControl Content="{Binding SidebarContent}"/>
<Grid RowDefinitions="Auto, *">
<UserControl Grid.Row="0" Margin="10, 20, 10, 10" VerticalAlignment="Center"
Content="{Binding SidebarContent}" />
<TextBox Grid.Row="1" Margin="5" Watermark="Notes..."
Text="{Binding SelectedCoin.Notes}"
IsReadOnly="False" TextWrapping="Wrap" />
</Grid>
</SplitView.Pane>
<UserControl Content="{Binding MainContent}"/>
<Grid RowDefinitions="*, Auto" ShowGridLines="False">
<DataGrid Grid.Row="0" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{Binding DisplayedList}"
SelectedItem="{Binding SelectedCoin, Mode=TwoWay}"
AutoGenerateColumns="False"
CanUserResizeColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Series" Width="*"
Binding="{Binding SeriesName}"
IsReadOnly="True" />
<DataGridTextColumn Header="Year"
Binding="{Binding Year}"
IsReadOnly="True" />
<DataGridTextColumn Header="Mint" Width="*"
Binding="{Binding MintMark}"
IsReadOnly="True" />
<DataGridTextColumn Header="Denomination" Width="*"
Binding="{Binding Denomination}"
IsReadOnly="True" />
<DataGridTextColumn Header="Variety" Width="*"
Binding="{Binding MajorVariety}"
IsReadOnly="True" />
<DataGridTextColumn Header="Grade" Width="*"
Binding="{Binding Grade}"
IsReadOnly="True" />
<DataGridTextColumn Header="Designation" Width="*"
Binding="{Binding Designation}"
IsReadOnly="True" />
<DataGridTextColumn Header="Est. Price" Width="*"
Binding="{Binding PriceGuideValue, StringFormat='${0:0.00}'}"
IsReadOnly="True" />
<DataGridTextColumn Header="PCGS #" Width="*"
Binding="{Binding PcgsNumber}"
IsReadOnly="True" />
<DataGridTextColumn Header="Quantity" Width="*"
Binding="{Binding Quantity}"
IsReadOnly="False" />
<DataGridTextColumn Header="Total Price" Width="*"
Binding="{Binding TotalPrice, StringFormat='${0:0.00}'}"
IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
<DockPanel Grid.Row="1">
<Grid HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="5" ColumnDefinitions="Auto, Auto"
DockPanel.Dock="Left">
<Button Grid.Column="0" Margin="5" Width="100" HorizontalContentAlignment="Center" Content="Delete"
Command="{Binding DeleteCommand}" />
</Grid>
<Grid HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="5" ColumnDefinitions="Auto, Auto"
DockPanel.Dock="Right">
<Label Grid.Column="0" Margin="5" VerticalAlignment="Center"
Content="{Binding TotalValue, StringFormat='Total Value: ${0:0.00}'}" />
<Button Grid.Column="1" Margin="5" Width="150" HorizontalContentAlignment="Center"
Content="Export to CSV..." Command="{Binding ExportCommand}" />
</Grid>
</DockPanel>
</Grid>
</SplitView>
</Window>
+55 -22
View File
@@ -1,28 +1,61 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System;
using System.Reactive;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using Avalonia.ReactiveUI;
using PcgsInvUi.ViewModels;
using ReactiveUI;
namespace PcgsInvUi.Views;
public partial class MainWindow : Window
{
// TODO New window.
// TODO Edit window.
// TODO Delete window.
// TODO Find window.
// TODO Export window.
// TODO Error handling.
// TODO Manual handling of API key.
// TODO Use CoinFacts link.
// TODO Export to CSV.
// TODO Take giant sheet of coins to integrate into the application.
// TODO Update all coins.
// TODO API request tracker (1000 per day)
// TODO API update schedules (per month? manual override? needs to store a date of last update in the coin object)
public MainWindow()
{
public partial class MainWindow : ReactiveWindow<MainWindowViewModel> {
public MainWindow() {
InitializeComponent();
this.WhenActivated(d => d(ViewModel!.ShowDeleteWindow.RegisterHandler(ShowDeleteWindowAsync)));
this.WhenActivated(d => d(ViewModel!.ShowExportWindow.RegisterHandler(ShowFilePickerAsync)));
this.WhenActivated(d => d(ViewModel!.ShowErrorWindow.RegisterHandler(ShowErrorWindowAsync)));
// this.WhenActivated(d => d(ViewModel!.ShowFindWindow.RegisterHandler(ShowFindWindowAsync)));
}
private async Task ShowDeleteWindowAsync(InteractionContext<DeleteWindowViewModel, bool> interaction) {
var window = new DeleteWindow();
window.DataContext = interaction.Input;
var result = await window.ShowDialog<bool>(this);
interaction.SetOutput(result);
}
private async Task ShowFindWindowAsync(InteractionContext<FindWindowViewModel, int> interaction) {
var window = new FindWindow();
window.DataContext = interaction.Input;
var result = await window.ShowDialog<int>(this);
interaction.SetOutput(result);
}
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",
ShowOverwritePrompt = true,
SuggestedFileName = "coins.csv",
SuggestedStartLocation = await StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
});
if (file is not null)
interationContext.SetOutput(file.Path);
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;
var result = await window.ShowDialog<bool>(this);
interaction.SetOutput(result);
}
}
+13
View File
@@ -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();
}
}
+9 -8
View File
@@ -7,19 +7,20 @@
x:Class="PcgsInvUi.Views.NewView" x:DataType="viewmodels:NewViewModel">
<DockPanel>
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto, *" RowDefinitions="*, *, *">
<Label Grid.Column="0" Grid.Row="0" Margin="5, 5, 20, 5" VerticalAlignment="Center" Content="PCGS #:"/>
<TextBox Grid.Column="1" Grid.Row="0" Margin="5, 5, 10, 5" Text="{Binding PcgsNumber}"/>
<Label Grid.Column="0" Grid.Row="1" Margin="5, 5, 20, 5" VerticalAlignment="Center" Content="Grade:"/>
<Label Grid.Column="0" Grid.Row="0" Margin="5, 5, 20, 5" VerticalAlignment="Center" Content="PCGS #:" />
<TextBox Grid.Column="1" Grid.Row="0" Margin="5, 5, 10, 5" Text="{Binding PcgsNumber}" />
<Label Grid.Column="0" Grid.Row="1" Margin="5, 5, 20, 5" VerticalAlignment="Center" Content="Grade:" />
<ComboBox Name="GradeChoices" Grid.Column="1" Grid.Row="1" SelectedIndex="0" Margin="5, 5, 10, 5"
ItemsSource="{Binding GradesList}" SelectedItem="{Binding Grade}"/>
<Label Grid.Column="0" Grid.Row="2" Margin="5, 5, 20, 5" VerticalAlignment="Center" Content="Quantity:"/>
<TextBox Grid.Column="1" Grid.Row="2" Margin="5, 5, 10, 5" Text="{Binding Quantity}"/>
Width="150"
ItemsSource="{Binding GradesList}" SelectedItem="{Binding Grade}" />
<Label Grid.Column="0" Grid.Row="2" Margin="5, 5, 20, 5" VerticalAlignment="Center" Content="Quantity:" />
<TextBox Grid.Column="1" Grid.Row="2" Margin="5, 5, 10, 5" Text="{Binding Quantity}" />
</Grid>
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="*, *">
<Button Grid.Column="0" Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"
Width="90" HorizontalContentAlignment="Center" Content="OK" Command="{Binding OkCommand}"/>
Width="90" HorizontalContentAlignment="Center" Content="OK" Command="{Binding OkCommand}" />
<Button Grid.Column="1" Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"
Width="90" HorizontalContentAlignment="Center" Content="Cancel" Command="{Binding CancelCommand}" />
Width="90" HorizontalContentAlignment="Center" Content="Clear" Command="{Binding ClearCommand}" />
</Grid>
</DockPanel>
</UserControl>
+3 -7
View File
@@ -1,13 +1,9 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
namespace PcgsInvUi.Views;
public partial class NewView : UserControl
{
public NewView()
{
public partial class NewView : UserControl {
public NewView() {
InitializeComponent();
}
}
+1 -1
View File
@@ -12,7 +12,7 @@
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,12 +0,0 @@
{
"runtimeOptions": {
"tfm": "net6.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
},
"configProperties": {
"System.Runtime.InteropServices.BuiltInComInterop.IsSupported": true
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 212 files, more files were not shown because too many files have changed in this diff. Show more