Add initial project structure with basic view models, views, and resources
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user