Add initial project structure with basic view models, views, and resources

This commit is contained in:
2026-09-14 19:46:46 +03:00
commit 45401645c6
68 changed files with 2538 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:semi="https://irihi.tech/semi"
xmlns:u="https://irihi.tech/ursa"
xmlns:material="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:avalonia="clr-namespace:LiveChartsCore.SkiaSharpView.Avalonia;assembly=LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:painting="clr-namespace:LiveChartsCore.SkiaSharpView.Painting;assembly=LiveChartsCore.SkiaSharpView"
x:Class="CryptoPortfolio_Tracker.App"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<semi:SemiTheme Locale="ru-RU"/>
<semi:UrsaSemiTheme Locale="ru-RU"/>
<semi:SemiPopupAnimations />
<material:MaterialIconStyles />
<Style Selector="Button.WithIcon">
<Setter Property="Width" Value="{Binding $self.Bounds.Height}" />
</Style>
<Style Selector="TextBox">
<Setter Property="Padding" Value="10" />
<Setter Property="CornerRadius" Value="10" />
</Style>
<Style Selector="Button">
<Setter Property="Padding" Value="10" />
<Setter Property="CornerRadius" Value="10" />
</Style>
<Style Selector="Button.ForSolidTheme">
<Setter Property="Padding" Value="10" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="{DynamicResource PrimaryColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource PrimaryColor}" />
</Style>
<Style Selector="NavigationPage">
<Style Selector="^ /template/ Border#PART_NavigationBar">
<Setter Property="Background" Value="{DynamicResource PrimaryColor}" />
</Style>
</Style>
<Style Selector="TableView /template/ ScrollViewer">
<Setter Property="VerticalScrollBarVisibility" Value="Hidden" />
</Style>
<Style Selector="avalonia|XamlLineSeries.Success">
<Setter Property="Stroke">
<Setter.Value>
<painting:SolidColorPaint Color="#3BB346" />
</Setter.Value>
</Setter>
</Style>
<Style Selector="avalonia|XamlLineSeries.Danger">
<Setter Property="Stroke">
<Setter.Value>
<painting:SolidColorPaint Color="#F93920" />
</Setter.Value>
</Setter>
</Style>
<Style Selector="u|CustomDialogControl.CryptoChartDialog">
<Setter Property="Width" Value="1000" />
<Setter Property="Height" Value="600" />
</Style>
</Application.Styles>
<Application.Resources>
<ResourceDictionary>
<SolidColorBrush x:Key="PrimaryColor" Color="DarkGreen" />
<StaticResource x:Key="NavigationPageBarBackground" ResourceKey="PrimaryColor" />
</ResourceDictionary>
</Application.Resources>
</Application>
+43
View File
@@ -0,0 +1,43 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using CryptoPortfolio_Tracker.ViewModels;
using CryptoPortfolio_Tracker.Views;
namespace CryptoPortfolio_Tracker;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
#if DEBUG
this.AttachDeveloperTools();
#endif
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
singleViewFactoryApplicationLifetime.MainViewFactory = () => new PageNavigationHost()
{
Page = new MainView { DataContext = new MainViewModel() }
};
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new PageNavigationHost()
{
Page = new MainView { DataContext = new MainViewModel() }
};
}
base.OnFrameworkInitializationCompleted();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@@ -0,0 +1,124 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace CryptoPortfolio_Tracker.Behaviors;
public static class WindowDragBehavior
{
public static readonly AttachedProperty<bool> EnableWindowDragProperty =
AvaloniaProperty.RegisterAttached<Control, bool>(
"EnableWindowDrag",
typeof(WindowDragBehavior));
public static bool GetEnableWindowDrag(Control element) =>
element.GetValue(EnableWindowDragProperty);
public static void SetEnableWindowDrag(Control element, bool value) =>
element.SetValue(EnableWindowDragProperty, value);
static WindowDragBehavior()
{
EnableWindowDragProperty.Changed.AddClassHandler<Control>(OnEnableWindowDragChanged);
}
private static void OnEnableWindowDragChanged(Control control, AvaloniaPropertyChangedEventArgs e)
{
if (e.NewValue is true)
{
// Контрол ещё может не быть в визуальном дереве
if (control.IsLoaded)
Attach(control);
else
control.Loaded += OnControlLoaded;
}
else
{
control.Loaded -= OnControlLoaded;
Detach(control);
}
}
private static void OnControlLoaded(object? sender, RoutedEventArgs e)
{
if (sender is Control control)
{
control.Loaded -= OnControlLoaded;
Attach(control);
}
}
private static void Attach(Control control)
{
var topBar = FindInVisualTree<Border>(control, "PART_NavigationBar");
if (topBar is null)
return;
topBar.AddHandler(
InputElement.PointerPressedEvent,
OnTopBarPointerPressed,
RoutingStrategies.Tunnel);
}
private static void Detach(Control control)
{
var topBar = FindInVisualTree<Border>(control, "Border#PART_NavigationBar");
if (topBar is null)
return;
topBar.RemoveHandler(
InputElement.PointerPressedEvent,
OnTopBarPointerPressed);
}
private static void OnTopBarPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
return;
// Проверяем — нажатие было на кнопке или интерактивном элементе?
if (e.Source is Visual source && IsInteractiveElement(source))
return;
if (sender is Visual visual &&
TopLevel.GetTopLevel(visual) is Window window)
{
window.BeginMoveDrag(e);
}
}
private static bool IsInteractiveElement(Visual visual)
{
Visual? current = visual;
while (current is not null)
{
if (current is Button or ToggleButton or CheckBox or
RadioButton or Slider or TextBox or ComboBox)
return true;
if (current is Border { Name: "Border#PART_NavigationBar" })
break;
current = current.GetVisualParent();
}
return false;
}
private static T? FindInVisualTree<T>(Visual root, string name) where T : Visual
{
if (root is T typed && root.Name == name)
return typed;
foreach (var child in root.GetVisualChildren())
{
var result = FindInVisualTree<T>(child, name);
if (result is not null)
return result;
}
return null;
}
}
@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using CryptoPortfolio_Tracker.Models;
using Microsoft.EntityFrameworkCore;
namespace CryptoPortfolio_Tracker.Context;
public partial class CryptoContext : DbContext
{
public CryptoContext()
{
}
public CryptoContext(DbContextOptions<CryptoContext> options)
: base(options)
{
}
public virtual DbSet<CryptoType> CryptoTypes { get; set; }
public virtual DbSet<Cryptocurrency> Cryptocurrencies { get; set; }
public virtual DbSet<Network> Networks { get; set; }
public virtual DbSet<Transaction> Transactions { get; set; }
public virtual DbSet<TransactionType> TransactionTypes { get; set; }
public virtual DbSet<TransactionsStatus> TransactionsStatuses { get; set; }
public virtual DbSet<User> Users { get; set; }
public virtual DbSet<UserWallet> UserWallets { get; set; }
public virtual DbSet<WalletType> WalletTypes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
=> optionsBuilder.UseNpgsql("Host=denjir-host.ru;Database=crypto;Username=crypto;Password=crypto;");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CryptoType>(entity =>
{
entity.HasKey(e => e.Id).HasName("crypto_types_pkey");
entity.ToTable("crypto_types");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name).HasColumnName("name");
});
modelBuilder.Entity<Cryptocurrency>(entity =>
{
entity.HasKey(e => e.Id).HasName("cryptocurrencies_pkey");
entity.ToTable("cryptocurrencies");
entity.HasIndex(e => e.CoingeckoId, "cryptocurrencies_coingecko_id_key").IsUnique();
entity.HasIndex(e => e.Symbol, "cryptocurrencies_symbol_key").IsUnique();
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.CoingeckoId).HasColumnName("coingecko_id");
entity.Property(e => e.CryptoTypeId).HasColumnName("crypto_type_id");
entity.Property(e => e.Name).HasColumnName("name");
entity.Property(e => e.NetworkId).HasColumnName("network_id");
entity.Property(e => e.Symbol).HasColumnName("symbol");
entity.HasOne(d => d.CryptoType).WithMany(p => p.Cryptocurrencies)
.HasForeignKey(d => d.CryptoTypeId)
.HasConstraintName("cryptocurrencies_crypto_type_id_fkey");
entity.HasOne(d => d.Network).WithMany(p => p.Cryptocurrencies)
.HasForeignKey(d => d.NetworkId)
.HasConstraintName("cryptocurrencies_networks_fk");
});
modelBuilder.Entity<Network>(entity =>
{
entity.HasKey(e => e.Id).HasName("networks_pk");
entity.ToTable("networks");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name).HasColumnName("name");
});
modelBuilder.Entity<Transaction>(entity =>
{
entity.HasKey(e => e.Id).HasName("transactions_pkey");
entity.ToTable("transactions");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Amount)
.HasPrecision(30, 18)
.HasColumnName("amount");
entity.Property(e => e.CryptocurrencyId).HasColumnName("cryptocurrency_id");
entity.Property(e => e.FeeUsd)
.HasPrecision(30, 2)
.HasColumnName("fee_usd");
entity.Property(e => e.PriceUsd)
.HasPrecision(24, 10)
.HasColumnName("price_usd");
entity.Property(e => e.RecipientAddress).HasColumnName("recipient_address");
entity.Property(e => e.SenderAddress).HasColumnName("sender_address");
entity.Property(e => e.StatusId).HasColumnName("status_id");
entity.Property(e => e.TotalUsd)
.HasPrecision(30, 2)
.HasColumnName("total_usd");
entity.Property(e => e.TransactionDate)
.HasDefaultValueSql("now()")
.HasColumnName("transaction_date");
entity.Property(e => e.TransactionHash).HasColumnName("transaction_hash");
entity.Property(e => e.TransactionTypeId).HasColumnName("transaction_type_id");
entity.Property(e => e.WalletId).HasColumnName("wallet_id");
entity.HasOne(d => d.Cryptocurrency).WithMany(p => p.Transactions)
.HasForeignKey(d => d.CryptocurrencyId)
.HasConstraintName("transactions_cryptocurrency_id_fkey");
entity.HasOne(d => d.Status).WithMany(p => p.Transactions)
.HasForeignKey(d => d.StatusId)
.HasConstraintName("transactions_status_id_fkey");
entity.HasOne(d => d.TransactionType).WithMany(p => p.Transactions)
.HasForeignKey(d => d.TransactionTypeId)
.HasConstraintName("transactions_transaction_type_id_fkey");
entity.HasOne(d => d.Wallet).WithMany(p => p.Transactions)
.HasForeignKey(d => d.WalletId)
.HasConstraintName("transactions_wallet_id_fkey");
});
modelBuilder.Entity<TransactionType>(entity =>
{
entity.HasKey(e => e.Id).HasName("transaction_types_pkey");
entity.ToTable("transaction_types");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name).HasColumnName("name");
});
modelBuilder.Entity<TransactionsStatus>(entity =>
{
entity.HasKey(e => e.Id).HasName("transactions_statuses_pkey");
entity.ToTable("transactions_statuses");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name).HasColumnName("name");
});
modelBuilder.Entity<User>(entity =>
{
entity.HasKey(e => e.Id).HasName("users_pkey");
entity.ToTable("users");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Email).HasColumnName("email");
entity.Property(e => e.Login).HasColumnName("login");
entity.Property(e => e.Password).HasColumnName("password");
});
modelBuilder.Entity<UserWallet>(entity =>
{
entity.HasKey(e => e.Id).HasName("user_wallets_pkey");
entity.ToTable("user_wallets");
entity.HasIndex(e => e.Address, "user_wallets_address_key").IsUnique();
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Address).HasColumnName("address");
entity.Property(e => e.Amount)
.HasPrecision(30, 12)
.HasColumnName("amount");
entity.Property(e => e.CryptocurrencyId).HasColumnName("cryptocurrency_id");
entity.Property(e => e.Name).HasColumnName("name");
entity.Property(e => e.TypeId).HasColumnName("type_id");
entity.Property(e => e.UserId).HasColumnName("user_id");
entity.HasOne(d => d.Cryptocurrency).WithMany(p => p.UserWallets)
.HasForeignKey(d => d.CryptocurrencyId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("user_wallets_cryptocurrencies_fk");
entity.HasOne(d => d.Type).WithMany(p => p.UserWallets)
.HasForeignKey(d => d.TypeId)
.HasConstraintName("user_wallets_type_id_fkey");
entity.HasOne(d => d.User).WithMany(p => p.UserWallets)
.HasForeignKey(d => d.UserId)
.HasConstraintName("user_wallets_user_id_fkey");
});
modelBuilder.Entity<WalletType>(entity =>
{
entity.HasKey(e => e.Id).HasName("wallet_types_pkey");
entity.ToTable("wallet_types");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name).HasColumnName("name");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
+20
View File
@@ -0,0 +1,20 @@
using System.Net.Http;
using System.Net.Http.Headers;
using CryptoPortfolio_Tracker.Models;
namespace CryptoPortfolio_Tracker.Core;
public static class AppState
{
public static User? CurrentUser { get; set; } = null;
public static HttpClient CryptoHttpClient { get; }
static AppState()
{
CryptoHttpClient = new HttpClient();
CryptoHttpClient.DefaultRequestHeaders.Add("x-cg-demo-api-key", "CG-tRuAG4pgWucXgGYSpZMi7ADN");
CryptoHttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
CryptoHttpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("CryptoPortfolioApp", "1.0"));
}
}
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="12.1.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.2" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.2" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="Irihi.Ursa" Version="2.2.0" />
<PackageReference Include="Irihi.Ursa.Themes.Semi" Version="2.2.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.1.0-dev-798" />
<PackageReference Include="Material.Icons.Avalonia" Version="3.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageReference Include="Semi.Avalonia" Version="12.1.0.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace CryptoPortfolio_Tracker.Models;
public partial class CryptoType
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public virtual ICollection<Cryptocurrency> Cryptocurrencies { get; set; } = new List<Cryptocurrency>();
}
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations.Schema;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using LiveChartsCore.Defaults;
namespace CryptoPortfolio_Tracker.Models;
public partial class Cryptocurrency : ObservableObject
{
public long Id { get; set; }
[ObservableProperty] public partial string Symbol { get; set; } = null!;
[ObservableProperty] public partial string Name { get; set; } = null!;
[ObservableProperty] public partial int CryptoTypeId { get; set; }
[ObservableProperty] public partial long NetworkId { get; set; }
[ObservableProperty] public partial string CoingeckoId { get; set; } = null!;
[ObservableProperty] public partial CryptoType CryptoType { get; set; } = null!;
[ObservableProperty] public partial Network Network { get; set; } = null!;
[ObservableProperty] public partial ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
[ObservableProperty] public partial ICollection<UserWallet> UserWallets { get; set; } = new List<UserWallet>();
[NotMapped] [ObservableProperty] public partial decimal? PriceChange24H { get; set; }
[NotMapped]
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsPositive))]
[NotifyPropertyChangedFor(nameof(IsNegative))]
public partial decimal? PriceChangePercentage24H { get; set; }
[NotMapped] [ObservableProperty] public partial decimal CurrentPriceUsd { get; set; }
[NotMapped] [ObservableProperty] public partial DateTime? PriceUpdatedAt { get; set; }
[NotMapped] [ObservableProperty] public partial decimal? MarketCapUsd { get; set; }
[NotMapped] [ObservableProperty] public partial decimal? Volume24HUsd { get; set; }
[NotMapped] [ObservableProperty] public partial Bitmap? Image { get; set; }
[NotMapped] public bool IsPositive => PriceChangePercentage24H > 0;
[NotMapped] public bool IsNegative => PriceChangePercentage24H < 0;
[NotMapped] public ObservableCollection<DateTimePoint> PriceHistory24H { get; set; } = [];
[NotMapped] public ObservableCollection<DateTimePoint> PriceHistory7D { get; set; } = [];
}
+13
View File
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace CryptoPortfolio_Tracker.Models;
public partial class Network
{
public long Id { get; set; }
public string Name { get; set; } = null!;
public virtual ICollection<Cryptocurrency> Cryptocurrencies { get; set; } = new List<Cryptocurrency>();
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace CryptoPortfolio_Tracker.Models;
public partial class Transaction
{
public long Id { get; set; }
public long CryptocurrencyId { get; set; }
public long WalletId { get; set; }
public DateTime TransactionDate { get; set; }
public int TransactionTypeId { get; set; }
public decimal Amount { get; set; }
public decimal PriceUsd { get; set; }
public decimal TotalUsd { get; set; }
public decimal FeeUsd { get; set; }
public string? TransactionHash { get; set; }
public int StatusId { get; set; }
public string? SenderAddress { get; set; }
public string? RecipientAddress { get; set; }
public virtual Cryptocurrency Cryptocurrency { get; set; } = null!;
public virtual TransactionsStatus Status { get; set; } = null!;
public virtual TransactionType TransactionType { get; set; } = null!;
public virtual UserWallet Wallet { get; set; } = null!;
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace CryptoPortfolio_Tracker.Models;
public partial class TransactionType
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public virtual ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace CryptoPortfolio_Tracker.Models;
public partial class TransactionsStatus
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public virtual ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
}
+17
View File
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
namespace CryptoPortfolio_Tracker.Models;
public partial class User
{
public long Id { get; set; }
public string Login { get; set; } = null!;
public string Email { get; set; } = null!;
public string Password { get; set; } = null!;
public virtual ICollection<UserWallet> UserWallets { get; set; } = new List<UserWallet>();
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace CryptoPortfolio_Tracker.Models;
public partial class UserWallet
{
public long Id { get; set; }
public string Name { get; set; } = null!;
public string Address { get; set; } = null!;
public int TypeId { get; set; }
public long UserId { get; set; }
public long? CryptocurrencyId { get; set; }
public decimal Amount { get; set; }
public virtual Cryptocurrency? Cryptocurrency { get; set; }
public virtual ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
public virtual WalletType Type { get; set; } = null!;
public virtual User User { get; set; } = null!;
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace CryptoPortfolio_Tracker.Models;
public partial class WalletType
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public virtual ICollection<UserWallet> UserWallets { get; set; } = new List<UserWallet>();
}
@@ -0,0 +1,131 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CryptoPortfolio_Tracker.Models;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
namespace CryptoPortfolio_Tracker.ViewModels;
public partial class CryptoChartViewModel : ViewModelBase
{
[ObservableProperty] public partial string CryptocurrencyName { get; set; } = null!;
[ObservableProperty] public partial decimal CurrentPrice { get; set; }
[ObservableProperty] public partial decimal PriceChangePercentage24H { get; set; }
[ObservableProperty] public partial decimal PriceChange24H { get; set; }
[ObservableProperty] public partial decimal? MarketCap { get; set; }
[ObservableProperty] public partial decimal? Volume24H { get; set; }
[ObservableProperty] public partial bool IsPositive { get; set; }
[ObservableProperty] public partial bool IsNegative { get; set; }
[ObservableProperty] public partial DateTime StartDate { get; set; }
[ObservableProperty] public partial DateTime EndDate { get; set; }
public ObservableCollection<DateTimePoint> PriceHistory7D { get; } = [];
public TimeSpan UnitWidth { get; } = TimeSpan.FromHours(1);
public Func<DateTime, string> DateFormatter { get; } =
date => date.ToString("dd.MM HH:mm");
public Func<double, string> PriceFormatter { get; } =
FormatPrice;
public SolidColorPaint GridPaint { get; } =
new(new SKColor(255, 255, 255, 25));
public SolidColorPaint ChartStroke { get; } =
new(new SKColor(0x3B, 0xB3, 0x46));
public ISeries[] Series { get; private set; } = [];
public CryptoChartViewModel(Cryptocurrency cryptocurrency)
{
CryptocurrencyName = cryptocurrency.Name;
CurrentPrice = cryptocurrency.CurrentPriceUsd;
PriceChangePercentage24H = cryptocurrency.PriceChangePercentage24H ?? 0;
PriceChange24H = cryptocurrency.PriceChange24H ?? 0;
MarketCap = cryptocurrency.MarketCapUsd;
Volume24H = cryptocurrency.Volume24HUsd;
IsPositive = cryptocurrency.PriceChangePercentage24H > 0;
IsNegative = cryptocurrency.PriceChangePercentage24H < 0;
foreach (var point in cryptocurrency.PriceHistory7D)
{
PriceHistory7D.Add(point);
}
if (PriceHistory7D.Count > 0)
{
StartDate = PriceHistory7D[0].DateTime;
EndDate = PriceHistory7D[^1].DateTime;
}
UpdateSeries();
}
partial void OnStartDateChanged(DateTime value)
{
UpdateSeries();
}
partial void OnEndDateChanged(DateTime value)
{
UpdateSeries();
}
private void UpdateSeries()
{
ObservableCollection<DateTimePoint> visiblePoints = [];
foreach (var point in PriceHistory7D)
{
if (point.DateTime >= StartDate &&
point.DateTime <= EndDate)
{
visiblePoints.Add(point);
}
}
if (visiblePoints.Count == 0)
{
Series = [];
OnPropertyChanged(nameof(Series));
return;
}
LineSeries<DateTimePoint> lineSeries =
new()
{
Values = visiblePoints,
GeometrySize = 0,
Fill = null,
GeometryFill = null,
GeometryStroke = null,
Stroke = ChartStroke,
LineSmoothness = 0.2
};
Series =
[
lineSeries
];
OnPropertyChanged(nameof(Series));
}
private static string FormatPrice(double price)
{
return price switch
{
>= 1000 => price.ToString("N0", CultureInfo.InvariantCulture),
>= 1 => price.ToString("N2", CultureInfo.InvariantCulture),
>= 0.01 => price.ToString("N4", CultureInfo.InvariantCulture),
_ => price.ToString("0.########", CultureInfo.InvariantCulture)
};
}
}
@@ -0,0 +1,376 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CryptoPortfolio_Tracker.Context;
using CryptoPortfolio_Tracker.Core;
using CryptoPortfolio_Tracker.Models;
using CryptoPortfolio_Tracker.Views;
using LiveChartsCore.Defaults;
using Microsoft.EntityFrameworkCore;
using Ursa.Controls;
namespace CryptoPortfolio_Tracker.ViewModels;
public partial class HomeViewModel : ViewModelBase
{
private readonly Dictionary<string, Bitmap> _imageCache = [];
private readonly Bitmap _placeholderImage;
public ObservableCollection<Cryptocurrency> Cryptocurrencies { get; set; } = [];
[ObservableProperty] public partial decimal DollarBalance { get; set; }
[ObservableProperty] public partial decimal BitcoinBalance { get; set; }
[ObservableProperty] public partial decimal ProfitLoss { get; set; }
[ObservableProperty] public partial string CoinNames { get; set; } = null!;
[ObservableProperty]
public partial string UserName { get; set; } = AppState.CurrentUser?.Email ?? "example@mail.com";
[ObservableProperty] public partial bool IsCryptoLoading { get; set; } = false;
public HomeViewModel()
{
_placeholderImage =
new Bitmap(AssetLoader.Open(new Uri("avares://CryptoPortfolio Tracker/Assets/coin-placeholder.png")));
_ = LoadCryptocurrenciesCommand.ExecuteAsync(null);
}
[RelayCommand]
private async Task LoadCryptocurrenciesAsync()
{
IsCryptoLoading = true;
try
{
await using CryptoContext context = new();
var cryptocurrencies = await context.Cryptocurrencies
.Include(x => x.Network)
.ToListAsync();
var coinIds = cryptocurrencies
.Select(x => x.CoingeckoId)
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToList();
var ids = string.Join(",", coinIds);
var url =
"https://api.coingecko.com/api/v3/coins/markets" +
"?vs_currency=usd" +
$"&ids={Uri.EscapeDataString(ids)}" +
"&order=market_cap_desc" +
"&per_page=250" +
"&page=1" +
"&sparkline=false";
using var response = await AppState.CryptoHttpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"API error: {(int)response.StatusCode} " +
$"{response.ReasonPhrase}\n{content}");
}
var geckoMarkets =
JsonSerializer.Deserialize<List<CoinGeckoMarket>>(
content,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? [];
Cryptocurrencies.Clear();
foreach (var r in geckoMarkets)
{
var crypto = cryptocurrencies.First(c => c.CoingeckoId == r.Id);
var currentPrice = r.CurrentPrice ?? 0m;
var changePercentage = r.PriceChangePercentage24H ?? 0m;
crypto.PriceChange24H = currentPrice * changePercentage / 100m;
crypto.PriceChangePercentage24H = changePercentage;
crypto.CurrentPriceUsd = currentPrice;
crypto.MarketCapUsd = r.MarketCap;
crypto.Volume24HUsd = r.TotalVolume;
crypto.PriceUpdatedAt = r.LastUpdated?.UtcDateTime;
crypto.Image = _placeholderImage;
crypto.PriceHistory24H.Clear();
crypto.PriceHistory7D.Clear();
Cryptocurrencies.Add(crypto);
_ = LoadImageAsync(crypto, r.Image);
}
await LoadPriceHistoriesAsync(Cryptocurrencies);
var wallets = await context.UserWallets
.Include(x => x.Type)
.Include(x => x.Cryptocurrency)
.Where(x => x.UserId == AppState.CurrentUser!.Id)
.ToListAsync();
DollarBalance = 0;
BitcoinBalance = 0;
List<string> names = [];
foreach (var wallet in wallets)
{
var geckoCurrency = geckoMarkets.First(x => x.Id == wallet.Cryptocurrency!.CoingeckoId);
var geckoBitcoin = geckoMarkets.First(x => x.Id == "bitcoin");
var dollarBalance = wallet.Amount * (geckoCurrency.CurrentPrice ?? 0m);
var bitcoinBalance = dollarBalance / (geckoBitcoin.CurrentPrice ?? 1m);
DollarBalance += dollarBalance;
BitcoinBalance += bitcoinBalance;
if (!names.Contains(wallet.Cryptocurrency!.Name))
{
names.Add(wallet.Cryptocurrency.Name);
}
}
CoinNames = string.Join(", ", names);
}
catch (Exception ex)
{
MainView.Current.ShowToast(
$"Ошибка при загрузке криптовалют: {ex.Message}",
NotificationType.Error);
}
finally
{
IsCryptoLoading = false;
}
}
private async Task LoadPriceHistoriesAsync(
IEnumerable<Cryptocurrency> cryptocurrencies)
{
using SemaphoreSlim semaphore = new(4);
List<Task> tasks = [];
foreach (var cryptocurrency in cryptocurrencies)
{
tasks.Add(LoadPriceHistoryAsync(cryptocurrency, semaphore));
}
await Task.WhenAll(tasks);
}
private async Task LoadPriceHistoryAsync(
Cryptocurrency cryptocurrency,
SemaphoreSlim semaphore)
{
await semaphore.WaitAsync();
try
{
if (string.IsNullOrWhiteSpace(cryptocurrency.CoingeckoId))
{
return;
}
List<DateTimePoint> prices24H =
await LoadChartAsync(cryptocurrency.CoingeckoId, 1);
foreach (var point in ReducePricePoints(prices24H, 120))
{
cryptocurrency.PriceHistory24H.Add(point);
}
List<DateTimePoint> prices7D =
await LoadChartAsync(cryptocurrency.CoingeckoId, 7);
foreach (var point in prices7D)
{
cryptocurrency.PriceHistory7D.Add(point);
}
}
catch
{
// Ошибка одного графика не должна ломать загрузку всей таблицы.
}
finally
{
semaphore.Release();
}
}
private async Task<List<DateTimePoint>> LoadChartAsync(
string coingeckoId,
int days)
{
var url =
$"https://api.coingecko.com/api/v3/coins/" +
$"{Uri.EscapeDataString(coingeckoId)}" +
"/market_chart" +
"?vs_currency=usd" +
$"&days={days}";
using var response =
await AppState.CryptoHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
return [];
}
var content =
await response.Content.ReadAsStringAsync();
var chart =
JsonSerializer.Deserialize<CoinGeckoMarketChart>(
content,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (chart?.Prices is null || chart.Prices.Count == 0)
{
return [];
}
List<DateTimePoint> prices = [];
foreach (var point in chart.Prices)
{
if (point.Count < 2)
{
continue;
}
long timestamp = Convert.ToInt64(point[0]);
DateTime date =
DateTimeOffset
.FromUnixTimeMilliseconds(timestamp)
.LocalDateTime;
prices.Add(
new DateTimePoint(
date,
point[1]));
}
return prices;
}
private static List<DateTimePoint> ReducePricePoints(
List<DateTimePoint> points,
int maxPoints)
{
if (points.Count <= maxPoints)
{
return points;
}
List<DateTimePoint> result = [];
double step =
(double)(points.Count - 1) /
(maxPoints - 1);
for (int i = 0; i < maxPoints; i++)
{
int index =
(int)Math.Round(i * step);
if (index >= points.Count)
{
index = points.Count - 1;
}
result.Add(points[index]);
}
return result;
}
[RelayCommand]
private async Task LogoutAsync()
{
await MainView.Current.ReplaceAsync(
new LoginView
{
DataContext = new LoginViewModel()
});
}
private async Task LoadImageAsync(
Cryptocurrency crypto,
string? imageUrl)
{
if (string.IsNullOrWhiteSpace(imageUrl))
{
return;
}
if (_imageCache.TryGetValue(imageUrl, out var cachedImage))
{
crypto.Image = cachedImage;
return;
}
try
{
var imageBytes =
await AppState.CryptoHttpClient.GetByteArrayAsync(imageUrl);
await using var stream =
new MemoryStream(imageBytes);
var bitmap = new Bitmap(stream);
_imageCache[imageUrl] = bitmap;
crypto.Image = bitmap;
}
catch
{
crypto.Image = _placeholderImage;
}
}
private class CoinGeckoMarket
{
[JsonPropertyName("id")] public string Id { get; set; } = null!;
[JsonPropertyName("current_price")] public decimal? CurrentPrice { get; set; }
[JsonPropertyName("market_cap")] public decimal? MarketCap { get; set; }
[JsonPropertyName("total_volume")] public decimal? TotalVolume { get; set; }
[JsonPropertyName("last_updated")] public DateTimeOffset? LastUpdated { get; set; }
[JsonPropertyName("price_change_percentage_24h")]
public decimal? PriceChangePercentage24H { get; set; }
[JsonPropertyName("image")] public string? Image { get; set; }
}
private class CoinGeckoMarketChart
{
[JsonPropertyName("prices")] public List<List<double>> Prices { get; set; } = [];
}
}
@@ -0,0 +1,53 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CryptoPortfolio_Tracker.Views;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using CryptoPortfolio_Tracker.Context;
using CryptoPortfolio_Tracker.Core;
using Microsoft.EntityFrameworkCore;
namespace CryptoPortfolio_Tracker.ViewModels
{
public partial class LoginViewModel : ObservableValidator
{
[Required] [ObservableProperty] public partial string Login { get; set; } = "admin";
[Required] [ObservableProperty] public partial string Password { get; set; } = "admin";
[RelayCommand]
private async Task LoginAsync()
{
if (Validate())
{
await using CryptoContext context = new();
var user = await context.Users.FirstOrDefaultAsync(u =>
(u.Email == Login || u.Login == Login) && u.Password == Password);
if (user == null)
{
MainView.Current.ShowToast("Неверный логин или пароль", NotificationType.Error);
return;
}
AppState.CurrentUser = user;
await MainView.Current.ReplaceAsync(new HomeView
{
DataContext = new HomeViewModel()
});
}
}
private bool Validate()
{
ValidateAllProperties();
return !HasErrors;
}
}
}
@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace CryptoPortfolio_Tracker.ViewModels;
public partial class MainViewModel : ViewModelBase
{
}
@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace CryptoPortfolio_Tracker.ViewModels;
public partial class SettingsViewModel : ViewModelBase
{
[ObservableProperty] public partial string Greeting { get; set; } = "This is Settings";
}
@@ -0,0 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace CryptoPortfolio_Tracker.ViewModels;
public abstract class ViewModelBase : ObservableObject
{
}
@@ -0,0 +1,118 @@
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:vm="using:CryptoPortfolio_Tracker.ViewModels"
xmlns:u="https://irihi.tech/ursa"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DesignWidth="900" d:DesignHeight="600"
x:Class="CryptoPortfolio_Tracker.Views.CryptoChartView"
x:DataType="vm:CryptoChartViewModel">
<Border Background="#232429"
CornerRadius="14"
Padding="20">
<Grid RowDefinitions="Auto,Auto,*,Auto"
RowSpacing="15">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Spacing="3">
<TextBlock Text="{Binding CryptocurrencyName}"
FontSize="24"
FontWeight="Bold" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock Text="{Binding CurrentPrice, StringFormat='{}{0:N2} $'}"
FontSize="20"
FontWeight="SemiBold" />
<TextBlock Text="{Binding PriceChangePercentage24H, StringFormat='{}{0:+0.00;-0.00;0.00} %'}"
Classes.Success="{Binding IsPositive}"
Classes.Danger="{Binding IsNegative}"
FontSize="16"
VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,Auto,Auto"
ColumnSpacing="25">
<StackPanel Spacing="2">
<TextBlock Text="Изменение"
FontSize="12"
Opacity="0.6" />
<TextBlock Text="{Binding PriceChange24H, StringFormat='{}{0:+0.00;-0.00;0.00} $'}"
Classes.Success="{Binding IsPositive}"
Classes.Danger="{Binding IsNegative}"
FontWeight="SemiBold" />
</StackPanel>
<StackPanel Grid.Column="1"
Spacing="2">
<TextBlock Text="Рыночная капитализация"
FontSize="12"
Opacity="0.6" />
<TextBlock Text="{Binding MarketCap, StringFormat='{}{0:N0} $'}"
FontWeight="SemiBold" />
</StackPanel>
<StackPanel Grid.Column="2"
Spacing="2">
<TextBlock Text="Объём за 24ч"
FontSize="12"
Opacity="0.6" />
<TextBlock Text="{Binding Volume24H, StringFormat='{}{0:N0} $'}"
FontWeight="SemiBold" />
</StackPanel>
</Grid>
<lvc:CartesianChart
Grid.Row="2"
MinHeight="400"
LegendPosition="Hidden"
TooltipPosition="Top"
FindingStrategy="CompareOnlyX"
ZoomMode="X"
AnimationsSpeed="0"
DrawMarginFrame="{x:Null}"
Series="{Binding Series}">
<lvc:CartesianChart.XAxes>
<lvc:XamlDateTimeAxis
UnitWidth="{Binding UnitWidth}"
Interval="01:00:00"
DateFormatter="{Binding DateFormatter}"
SeparatorsPaint="{Binding GridPaint}" />
</lvc:CartesianChart.XAxes>
<lvc:CartesianChart.YAxes>
<lvc:XamlAxis
Labeler="{Binding PriceFormatter}"
SeparatorsPaint="{Binding GridPaint}" />
</lvc:CartesianChart.YAxes>
</lvc:CartesianChart>
<TextBlock
Grid.Row="3"
Text="Колесо мыши — масштабирование • Перетаскивание — перемещение"
FontSize="12"
Opacity="0.5"
HorizontalAlignment="Center" />
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CryptoPortfolio_Tracker.Views;
public partial class CryptoChartView : UserControl
{
public CryptoChartView()
{
InitializeComponent();
}
}
@@ -0,0 +1,209 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:avalonia="using:Material.Icons.Avalonia"
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:vm="using:CryptoPortfolio_Tracker.ViewModels"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:models="clr-namespace:CryptoPortfolio_Tracker.Models"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="720"
Header="CryptoProfile Tracker"
x:DataType="vm:HomeViewModel"
x:Class="CryptoPortfolio_Tracker.Views.HomeView">
<Design.DataContext>
<vm:HomeViewModel />
</Design.DataContext>
<ContentPage.Styles>
<Style Selector="Border.Card">
<Setter Property="Height" Value="100" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="10" />
<Setter Property="BorderBrush" Value="{StaticResource PrimaryColor}" />
<Setter Property="Background" Value="#232429" />
</Style>
</ContentPage.Styles>
<Grid RowDefinitions="Auto, Auto, Auto, *" RowSpacing="15" ShowGridLines="False">
<Grid ColumnDefinitions="*, Auto" Margin="15 15 15 0">
<TextBlock Text="{Binding UserName}" VerticalAlignment="Center"
FontWeight="Bold" FontSize="18" />
<Button Grid.Column="1" Classes="Danger" Theme="{StaticResource OutlineButton}"
Command="{Binding LogoutCommand}">
<avalonia:MaterialIcon Kind="Logout" />
</Button>
</Grid>
<Border ClipToBounds="True" Grid.Row="1" Margin="15 0">
<Grid ColumnDefinitions="*,*,*,*" ColumnSpacing="15">
<Border Classes="Card">
<Grid RowDefinitions="*,*" RowSpacing="10" VerticalAlignment="Center">
<TextBlock Text="Портфель" FontWeight="Bold" HorizontalAlignment="Center" TextWrapping="Wrap"
TextAlignment="Center" />
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Grid.Row="1" Text="{Binding DollarBalance, StringFormat='{}{0:F2}'}"
FontWeight="Bold" HorizontalAlignment="Center" TextWrapping="Wrap" />
<avalonia:MaterialIcon Kind="CurrencyUsd" Height="18" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
<Border Classes="Card" Grid.Column="1">
<Grid RowDefinitions="*,*" RowSpacing="10" VerticalAlignment="Center">
<TextBlock Text="Портфель" FontWeight="Bold" HorizontalAlignment="Center" TextWrapping="Wrap"
TextAlignment="Center" />
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="{Binding BitcoinBalance, StringFormat='{}{0:F2}'}" FontWeight="Bold"
HorizontalAlignment="Center" TextWrapping="Wrap" />
<avalonia:MaterialIcon Kind="CurrencyBtc" Height="18" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
<Border Classes="Card" Grid.Column="2">
<Grid RowDefinitions="*,*" RowSpacing="10" VerticalAlignment="Center">
<TextBlock Text="Прибыль/Убытки" FontWeight="Bold" HorizontalAlignment="Center"
TextWrapping="Wrap" TextAlignment="Center" />
<TextBlock Grid.Row="1" Text="{Binding ProfitLoss, StringFormat='{}{0:F2} $'}"
FontWeight="Bold" HorizontalAlignment="Center" TextWrapping="Wrap" />
</Grid>
</Border>
<Border Classes="Card" Grid.Column="3">
<Grid RowDefinitions="Auto,*" RowSpacing="10" VerticalAlignment="Center">
<TextBlock Text="Валюты" FontWeight="Bold" HorizontalAlignment="Center" TextWrapping="Wrap"
TextAlignment="Center" />
<TextBlock Grid.Row="1" Text="{Binding CoinNames}" FontWeight="Bold"
HorizontalAlignment="Center" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" />
</Grid>
</Border>
</Grid>
</Border>
<Border ClipToBounds="True" Grid.Row="2" Margin="20 0">
<Grid ColumnDefinitions="*, Auto, Auto, Auto, Auto, Auto" ColumnSpacing="10">
<TextBox PlaceholderText="Поиск" />
<DropDownButton Grid.Column="1" Foreground="{DynamicResource PrimaryColor}" CornerRadius="10"
Padding="10">
<DropDownButton.Flyout>
<MenuFlyout>
<ComboBox HorizontalAlignment="Stretch" Width="200" />
</MenuFlyout>
</DropDownButton.Flyout>
<avalonia:MaterialIcon Kind="Filter" />
</DropDownButton>
<Button Grid.Column="2" Classes="WithIcon" VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryColor}"
Command="{Binding LoadCryptocurrenciesCommand}">
<avalonia:MaterialIcon Kind="Refresh" />
</Button>
<Button Grid.Column="3" Classes="WithIcon" Theme="{DynamicResource SolidButton}"
Background="{DynamicResource PrimaryColor}"
BorderBrush="{StaticResource PrimaryColor}" CornerRadius="10" Padding="10">
<avalonia:MaterialIcon Kind="Plus" />
</Button>
<Button Grid.Column="4" Classes="WithIcon"
Foreground="{StaticResource PrimaryColor}" CornerRadius="10" Padding="10">
<avalonia:MaterialIcon Kind="Edit" />
</Button>
<Button Grid.Column="5" Classes="WithIcon Danger" Theme="{DynamicResource SolidButton}"
Background="{DynamicResource PrimaryColor}"
BorderBrush="{StaticResource PrimaryColor}" CornerRadius="10" Padding="10">
<avalonia:MaterialIcon Kind="Trash" />
</Button>
</Grid>
</Border>
<Grid Grid.Row="3" Margin="20 0 20 20">
<ProgressBar IsIndeterminate="True" VerticalAlignment="Top" Margin="0 -5 0 0" Height="1"
Foreground="{DynamicResource PrimaryColor}"
IsVisible="{Binding IsCryptoLoading}" />
<TableView x:Name="CryptoTable"
CornerRadius="10"
ItemsSource="{Binding Cryptocurrencies}"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
SelectionMode="Single"
SelectionChanged="CryptoTable_OnSelectionChanged">
<TableView.Columns>
<TableViewColumn Binding="{Binding Image}" CanUserResize="True" Width="50">
<TableViewColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding Image}" Margin="0" Height="17" />
</DataTemplate>
</TableViewColumn.CellTemplate>
</TableViewColumn>
<TableViewColumn Header="Символ"
Binding="{Binding Symbol}" Width="100" />
<TableViewColumn Header="Название"
Binding="{Binding Name}" Width="125" />
<TableViewColumn Header="Курс"
Binding="{Binding CurrentPriceUsd, StringFormat='{}{0:N2} $'}"
Width="135" />
<TableViewColumn Header="Изменения за 24ч"
Binding="{Binding PriceChange24H, StringFormat='{}{0:N2} $'}">
<TableViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Spacing="5" Orientation="Horizontal">
<TextBlock
Text="{Binding PriceChangePercentage24H, StringFormat='{}{0:N2} %'}"
Classes.Success="{Binding IsPositive}"
Classes.Danger="{Binding IsNegative}" />
<TextBlock
Text="{Binding PriceChange24H, StringFormat='{}({0:N2} $)'}"
Classes.Success="{Binding IsPositive}"
Classes.Danger="{Binding IsNegative}" />
</StackPanel>
</DataTemplate>
</TableViewColumn.CellTemplate>
</TableViewColumn>
<TableViewColumn Header="График за 24ч"
Width="160">
<TableViewColumn.CellTemplate>
<DataTemplate>
<Border Name="ChartBorder">
<lvc:CartesianChart
Width="140"
Height="17"
LegendPosition="Hidden"
TooltipPosition="Hidden"
ZoomMode="None"
Padding="0"
AnimationsSpeed="0:00:00">
<lvc:CartesianChart.Series>
<lvc:XamlLineSeries
Values="{Binding PriceHistory24H}"
GeometrySize="0"
Fill="{x:Null}"
GeometryFill="{x:Null}"
GeometryStroke="{x:Null}"
LineSmoothness="0.5"
Classes.Success="{Binding IsPositive}"
Classes.Danger="{Binding IsNegative}"/>
</lvc:CartesianChart.Series>
<lvc:CartesianChart.XAxes>
<lvc:XamlAxis IsVisible="False" />
</lvc:CartesianChart.XAxes>
<lvc:CartesianChart.YAxes>
<lvc:XamlAxis IsVisible="False" />
</lvc:CartesianChart.YAxes>
</lvc:CartesianChart>
</Border>
</DataTemplate>
</TableViewColumn.CellTemplate>
</TableViewColumn>
<TableViewColumn Header="Рыночная капитализация"
Binding="{Binding MarketCapUsd, StringFormat='{}{0:N2} $'}" />
<TableViewColumn Header="Объём торгов" Binding="{Binding Volume24HUsd, StringFormat='{}{0:N2} $'}" />
</TableView.Columns>
</TableView>
</Grid>
</Grid>
</ContentPage>
@@ -0,0 +1,55 @@
using Avalonia.Controls;
using CryptoPortfolio_Tracker.Models;
using CryptoPortfolio_Tracker.ViewModels;
using Ursa.Controls;
namespace CryptoPortfolio_Tracker.Views;
public partial class HomeView : ContentPage
{
public HomeView()
{
InitializeComponent();
}
private void CryptoTable_OnSelectionChanged(
object? sender,
SelectionChangedEventArgs e)
{
if (sender is not TableView tableView)
{
return;
}
if (tableView.SelectedItem is not Cryptocurrency cryptocurrency)
{
return;
}
CryptoChartViewModel viewModel =
new(cryptocurrency);
tableView.SelectedIndex = -1;
_ = OverlayDialog.ShowCustomAsync<
CryptoChartView,
CryptoChartViewModel,
object>(
viewModel,
null,
new OverlayDialogOptions
{
Title = cryptocurrency.Name,
CanDragMove = true,
CanResize = true,
CanLightDismiss = true,
IsCloseButtonVisible = true,
HorizontalScrollBarVisibility =
Avalonia.Controls.Primitives.ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility =
Avalonia.Controls.Primitives.ScrollBarVisibility.Disabled,
Buttons = DialogButton.None,
StyleClass = "CryptoChartDialog"
});
}
}
@@ -0,0 +1,36 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:avalonia="using:Material.Icons.Avalonia"
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:u="https://irihi.tech/ursa"
xmlns:viewmodels="using:CryptoPortfolio_Tracker.ViewModels"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="720"
x:Class="CryptoPortfolio_Tracker.Views.LoginView"
x:DataType="viewmodels:LoginViewModel"
Header="Вход"
NavigationPage.HasNavigationBar="False">
<Design.DataContext>
<viewmodels:LoginViewModel />
</Design.DataContext>
<Grid ColumnDefinitions="1.4*, *">
<Border HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="{DynamicResource PrimaryColor}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="0">
<avalonia:MaterialIcon Kind="Bitcoin" Width="400" HorizontalAlignment="Center" />
<TextBlock Text="CryptoPortfolio Tracker" FontSize="44" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" TextAlignment="Center" />
</StackPanel>
</Border>
<u:Form Grid.Column="1" Margin="40" VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<TextBlock FontSize="34" FontWeight="Bold" Foreground="White" HorizontalAlignment="Center"
TextAlignment="Center" Text="Добро пожаловать" />
<TextBox Text="{Binding Login}" u:FormItem.Label="Логин" />
<TextBox Text="{Binding Password}" u:FormItem.Label="Пароль" Classes="revealPasswordButton"/>
<Button Content="Войти" HorizontalAlignment="Stretch" Theme="{StaticResource SolidButton}"
Classes="ForSolidTheme" Command="{Binding LoginCommand}" />
</u:Form>
</Grid>
</ContentPage>
@@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CryptoPortfolio_Tracker.Views
{
public partial class LoginView : ContentPage
{
public LoginView()
{
InitializeComponent();
}
}
}
@@ -0,0 +1,17 @@
<NavigationPage 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:vm="using:CryptoPortfolio_Tracker.ViewModels"
xmlns:behaviors="clr-namespace:CryptoPortfolio_Tracker.Behaviors"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CryptoPortfolio_Tracker.Views.MainView"
x:DataType="vm:MainViewModel"
behaviors:WindowDragBehavior.EnableWindowDrag="True">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainViewModel />
</Design.DataContext>
</NavigationPage>
@@ -0,0 +1,45 @@
using Avalonia;
using Avalonia.Controls;
using CryptoPortfolio_Tracker.ViewModels;
using System;
using Avalonia.Controls.Notifications;
using Ursa.Controls;
namespace CryptoPortfolio_Tracker.Views;
public partial class MainView : NavigationPage
{
public static MainView Current { get; set; } = null!;
private WindowToastManager _toastManager = null!;
public MainView()
{
InitializeComponent();
Current = this;
}
protected override async void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_toastManager = new WindowToastManager(TopLevel.GetTopLevel(this));
if (CurrentPage == null)
await PushAsync(new LoginView()
{
DataContext = new LoginViewModel()
});
}
public void ShowToast(string message, NotificationType type)
{
_toastManager.Show(
content: message,
showIcon: true,
showClose: true,
type: type,
expiration: TimeSpan.FromSeconds(3),
classes: ["Light"]);
}
}
@@ -0,0 +1,18 @@
<u:UrsaWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:CryptoPortfolio_Tracker.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:CryptoPortfolio_Tracker.Views"
xmlns:u="https://irihi.tech/ursa"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CryptoPortfolio_Tracker.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="CryptoPortfolio Tracker"
MinWidth="1280"
MinHeight="720"
WindowStartupLocation="CenterScreen"
IsTitleBarVisible="False"
IsFullScreenButtonVisible="False">
<views:MainView />
</u:UrsaWindow>
@@ -0,0 +1,12 @@
using Avalonia.Controls;
using Ursa.Controls;
namespace CryptoPortfolio_Tracker.Views;
public partial class MainWindow : UrsaWindow
{
public MainWindow()
{
InitializeComponent();
}
}