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 _imageCache = []; private readonly Bitmap _placeholderImage; public ObservableCollection 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>( 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 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 cryptocurrencies) { using SemaphoreSlim semaphore = new(4); List 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 prices24H = await LoadChartAsync(cryptocurrency.CoingeckoId, 1); foreach (var point in ReducePricePoints(prices24H, 120)) { cryptocurrency.PriceHistory24H.Add(point); } List prices7D = await LoadChartAsync(cryptocurrency.CoingeckoId, 7); foreach (var point in prices7D) { cryptocurrency.PriceHistory7D.Add(point); } } catch { // Ошибка одного графика не должна ломать загрузку всей таблицы. } finally { semaphore.Release(); } } private async Task> 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( content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (chart?.Prices is null || chart.Prices.Count == 0) { return []; } List 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 ReducePricePoints( List points, int maxPoints) { if (points.Count <= maxPoints) { return points; } List 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> Prices { get; set; } = []; } }