diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index c0812307..74852074 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -12,6 +12,10 @@ on: paths: - "Daybreak/**" - "Daybreak.Installer/**" + - "Daybreak.API/**" + - "Daybreak.7ZipExtractor/**" + - "Daybreak.Installer/**" + - "Daybreak.Shared/**" workflow_dispatch: jobs: diff --git a/Daybreak.Shared/Services/BuildTemplates/BuildTemplateManager.cs b/Daybreak.Shared/Services/BuildTemplates/BuildTemplateManager.cs index a2419a61..b95cc9ff 100644 --- a/Daybreak.Shared/Services/BuildTemplates/BuildTemplateManager.cs +++ b/Daybreak.Shared/Services/BuildTemplates/BuildTemplateManager.cs @@ -558,10 +558,17 @@ private async Task> LoadBuildFromFile(string path } else { - build.Metadata = - JsonConvert.DeserializeObject>( - Encoding.UTF8.GetString( - Convert.FromBase64String(content[1]))); + try + { + build.Metadata = + JsonConvert.DeserializeObject>( + Encoding.UTF8.GetString( + Convert.FromBase64String(content[1]))); + } + catch(Exception ex) + { + return new InvalidOperationException("Failed to parse build metadata", ex); + } } } diff --git a/Daybreak/Services/Charts/LiveChartInitializer.cs b/Daybreak/Services/Charts/LiveChartInitializer.cs index 566ef6c1..5e0db0d1 100644 --- a/Daybreak/Services/Charts/LiveChartInitializer.cs +++ b/Daybreak/Services/Charts/LiveChartInitializer.cs @@ -1,7 +1,4 @@ -using Daybreak.Shared.Models.Metrics; -using Daybreak.Shared.Models.Trade; -using LiveChartsCore; -using LiveChartsCore.Kernel; +using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using System.Windows.Extensions.Services; @@ -19,14 +16,6 @@ public void OnStartup() config.AddSkiaSharp() .AddDefaultMappers() .AddDarkTheme() - .AddLightTheme() - .HasMap((metric, index) => - { - return new Coordinate(metric.Timestamp.Ticks, Convert.ToDouble(metric.Measurement)); - }) - .HasMap((quote, point) => - { - return new Coordinate(quote.Timestamp?.Ticks ?? 0, ((double)quote.Price) / 20d); - })); + .AddLightTheme()); } } diff --git a/Daybreak/Services/Guildwars/Utils/GuildwarsFileStream.cs b/Daybreak/Services/Guildwars/Utils/GuildwarsFileStream.cs index 6d6a006b..cc9a4a7f 100644 --- a/Daybreak/Services/Guildwars/Utils/GuildwarsFileStream.cs +++ b/Daybreak/Services/Guildwars/Utils/GuildwarsFileStream.cs @@ -1,6 +1,5 @@ using Daybreak.Services.Guildwars.Models; using Daybreak.Services.Guildwars.Utils; -using System.ComponentModel.DataAnnotations; using System.Core.Extensions; using System.IO; diff --git a/Daybreak/Services/Notifications/NotificationService.cs b/Daybreak/Services/Notifications/NotificationService.cs index d2bbf22b..b3464fc4 100644 --- a/Daybreak/Services/Notifications/NotificationService.cs +++ b/Daybreak/Services/Notifications/NotificationService.cs @@ -4,11 +4,13 @@ using Daybreak.Shared.Models.Notifications.Handling; using Daybreak.Shared.Services.Notifications; using Daybreak.Shared.Utils; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Slim; using System.Collections.Concurrent; using System.Core.Extensions; using System.Extensions; +using System.Extensions.Core; using System.Runtime.CompilerServices; namespace Daybreak.Services.Notifications; @@ -72,18 +74,26 @@ async Task INotificationProducer.OpenNotification(Notification notification, boo { if (storeNotification) { - await this.storage.OpenNotification(new NotificationDTO + try { - Title = notification.Title, - Description = notification.Description, - Id = notification.Id, - Level = (int)notification.Level, - MetaData = notification.Metadata, - HandlerType = notification.HandlingType?.AssemblyQualifiedName, - ExpirationTime = notification.ExpirationTime.ToSafeDateTimeOffset().ToUnixTimeMilliseconds(), - CreationTime = notification.CreationTime.ToSafeDateTimeOffset().ToUnixTimeMilliseconds(), - Closed = true - }, cancellationToken); + await this.storage.OpenNotification(new NotificationDTO + { + Title = notification.Title, + Description = notification.Description, + Id = notification.Id, + Level = (int)notification.Level, + MetaData = notification.Metadata, + HandlerType = notification.HandlingType?.AssemblyQualifiedName, + ExpirationTime = notification.ExpirationTime.ToSafeDateTimeOffset().ToUnixTimeMilliseconds(), + CreationTime = notification.CreationTime.ToSafeDateTimeOffset().ToUnixTimeMilliseconds(), + Closed = true + }, cancellationToken); + } + catch (Exception ex) + { + this.logger.CreateScopedLogger().LogError(ex, "Failed to open notification"); + return; + } } if (notification.HandlingType is null) @@ -91,8 +101,8 @@ await this.storage.OpenNotification(new NotificationDTO return; } - var handler = this.serviceManager.GetService(notification.HandlingType) as INotificationHandler; - handler?.OpenNotification(notification); + var handler = (INotificationHandler)this.serviceManager.GetRequiredService(notification.HandlingType); + handler.OpenNotification(notification); } async Task INotificationProducer.RemoveNotification(Notification notification, CancellationToken cancellationToken) diff --git a/Daybreak/Services/Notifications/NotificationStorage.cs b/Daybreak/Services/Notifications/NotificationStorage.cs index 57c5bb95..bf0f0733 100644 --- a/Daybreak/Services/Notifications/NotificationStorage.cs +++ b/Daybreak/Services/Notifications/NotificationStorage.cs @@ -1,53 +1,105 @@ using Daybreak.Services.Notifications.Models; +using Microsoft.Extensions.Logging; using System.Core.Extensions; +using System.Extensions.Core; namespace Daybreak.Services.Notifications; internal sealed class NotificationStorage( - NotificationsDbContext liteCollection) : INotificationStorage + NotificationsDbContext liteCollection, + ILogger logger) : INotificationStorage { private List? notificationsCache; private readonly NotificationsDbContext liteCollection = liteCollection.ThrowIfNull(); + private readonly ILogger logger = logger.ThrowIfNull(); public async ValueTask> GetPendingNotifications(CancellationToken cancellationToken) { - this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); - return this.notificationsCache - .Where(dto => dto.Closed == false && dto.ExpirationTime < DateTimeOffset.Now.ToUnixTimeMilliseconds()); + try + { + this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); + return this.notificationsCache + .Where(dto => dto.Closed == false && dto.ExpirationTime < DateTimeOffset.Now.ToUnixTimeMilliseconds()); + } + catch (Exception ex) + { + this.logger.CreateScopedLogger().LogError(ex, "Failed to get pending notifications"); + throw; + } } public async ValueTask> GetNotifications(CancellationToken cancellationToken) { - this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); - return this.notificationsCache; + try + { + this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); + return this.notificationsCache; + } + catch (Exception ex) + { + this.logger.CreateScopedLogger().LogError(ex, "Failed to get notifications"); + throw; + } } public async ValueTask StoreNotification(NotificationDTO notification, CancellationToken cancellationToken) { notification.ThrowIfNull(); - await this.liteCollection.Insert(notification, cancellationToken); - this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); - this.notificationsCache.Add(notification); + try + { + await this.liteCollection.Insert(notification, cancellationToken); + this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); + this.notificationsCache.Add(notification); + } + catch (Exception ex) + { + this.logger.CreateScopedLogger().LogError(ex, "Failed to store notification"); + throw; + } } public async ValueTask OpenNotification(NotificationDTO notificationDTO, CancellationToken cancellationToken) { notificationDTO.ThrowIfNull(); - notificationDTO.Closed = true; - await this.liteCollection.Update(notificationDTO, cancellationToken); - this.notificationsCache = await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); + try + { + notificationDTO.Closed = true; + await this.liteCollection.Update(notificationDTO, cancellationToken); + this.notificationsCache = await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); + } + catch (Exception ex) + { + this.logger.CreateScopedLogger().LogError(ex, "Failed to open notification"); + throw; + } } public async ValueTask RemoveNotification(NotificationDTO notificationDTO, CancellationToken cancellationToken) { - await this.liteCollection.Delete(notificationDTO.Id, cancellationToken); - this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); - this.notificationsCache.Remove(notificationDTO); + try + { + await this.liteCollection.Delete(notificationDTO.Id, cancellationToken); + this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken); + this.notificationsCache.Remove(notificationDTO); + } + catch (Exception ex) + { + this.logger.CreateScopedLogger().LogError(ex, "Failed to remove notification"); + throw; + } } public async ValueTask RemoveAllNotifications(CancellationToken cancellationToken) { - await this.liteCollection.DeleteAll(cancellationToken); - this.notificationsCache = default; + try + { + await this.liteCollection.DeleteAll(cancellationToken); + this.notificationsCache = default; + } + catch (Exception ex) + { + this.logger.CreateScopedLogger().LogError(ex, "Failed to remove all notifications"); + throw; + } } } diff --git a/Daybreak/Services/Screenshots/Models/Location.cs b/Daybreak/Services/Screenshots/Models/Location.cs index c21ff9c9..9c721866 100644 --- a/Daybreak/Services/Screenshots/Models/Location.cs +++ b/Daybreak/Services/Screenshots/Models/Location.cs @@ -114,15 +114,6 @@ internal sealed class Location IdFormat = "D2", StartIndex = 1, Count = 10 - }, - new Entry - { - Map = Map.GreenHillsCounty, - Url = "https://media.discordapp.net/attachments/279231165045407744/1053453780580044940/image.png?ex=656a6c49&is=6557f749&hm=beab3433db2cac2d519fc8158978a949e06a57df50b587f41afe3718dc4dad20&=&format=webp&width=954&height=521", - Credit = "", - IdFormat = "D2", - StartIndex = 1, - Count = 1 } ]); public static readonly Location Ascalon = new( @@ -262,15 +253,6 @@ internal sealed class Location IdFormat = "D2", StartIndex = 1, Count = 1 - }, - new Entry - { - Map = Map.TheGreatNorthernWall, - Url = "https://media.discordapp.net/attachments/279231165045407744/853784390357876746/gw005.jpg?ex=656d152d&is=655aa02d&hm=03e8464e9fc60f8c832eebc246a4fff422ebb865d27dc189b727f22aacab6fb2&=&format=webp&width=954&height=380", - Credit = "https://discordapp.com/users/Chrono#6655", - IdFormat = "D2", - StartIndex = 1, - Count = 1 } ]); public static readonly Location NorthernShiverpeaks = new( @@ -365,15 +347,6 @@ internal sealed class Location IdFormat = "D2", StartIndex = 1, Count = 7 - }, - new Entry - { - Map = Map.IronHorseMine, - Url = "https://media.discordapp.net/attachments/279231165045407744/927936695230414888/gw064.jpg?ex=656f3864&is=655cc364&hm=75456e75a3a77e0dcbc8b0f1d3dfa292e477b535e90c7a8f40566e0ffa52cb64&=&format=webp&width=903&height=564", - Credit = "https://discordapp.com/users/Pekka#4619", - IdFormat = "D2", - StartIndex = 1, - Count = 1 } ]); public static readonly Location Kryta = new( @@ -1025,15 +998,6 @@ internal sealed class Location IdFormat = "D2", StartIndex = 1, Count = 60 - }, - new Entry - { - Map = Map.TombOfThePrimevalKings, - Url = "https://cdn.discordapp.com/attachments/279231165045407744/1137832214151843930/gw079.jpg?ex=656cd953&is=655a6453&hm=af57440037882d84e959b950945b738e611d976a2f0beb24d9b1a0fcd626086d&", - Credit = "https://discordapp.com/users/Soldrand#2252", - IdFormat = "D2", - StartIndex = 1, - Count = 1 } ]); public static readonly Location SouthernShiverpeaks = new( @@ -1264,25 +1228,7 @@ internal sealed class Location IdFormat = "D2", StartIndex = 1, Count = 1 - }, - new Entry - { - Map = Map.TalusChute, - Url = "https://cdn.discordapp.com/attachments/279231165045407744/1079051027753480232/gw500.jpg?ex=656b4294&is=6558cd94&hm=95afe18ecddc81d5cedc9865e5db76986dc4ee2fe34f4832ffcc35854298f371&", - Credit = "https://discordapp.com/users/miragee#4827", - IdFormat = "D2", - StartIndex = 1, - Count = 1 - }, - new Entry - { - Map = Map.TalusChute, - Url = "https://cdn.discordapp.com/attachments/279231165045407744/1079051028000948345/gw507.jpg?ex=656b4294&is=6558cd94&hm=9e85f4560d8184fbb3a575515a72bdfb1d5812a64b57e486acb9f2a1ce658b4a&", - Credit = "https://discordapp.com/users/miragee#4827", - IdFormat = "D2", - StartIndex = 1, - Count = 1 - }, + } ]); public static readonly Location RingOfFireIslandChain = new( Region.RingOfFireIslands, @@ -2484,15 +2430,6 @@ internal sealed class Location IdFormat = "D2", StartIndex = 1, Count = 2 - }, - new Entry - { - Map = Map.SunquaVale, - Url = "https://media.discordapp.net/attachments/279231165045407744/859817527739547678/gw010.jpg?ex=657092f9&is=655e1df9&hm=b77dd201214212cfc6dd68db2218f550136878f6a2dca37830cc0dcad4201363&=&format=webp&width=954&height=479", - Credit = "https://discordapp.com/users/Sara#2170", - IdFormat = "D2", - StartIndex = 1, - Count = 1 } ]); public static readonly Location KainengCity = new( @@ -4204,15 +4141,6 @@ internal sealed class Location IdFormat = "D2", StartIndex = 1, Count = 5 - }, - new Entry - { - Map = Map.TheSulfurousWastes, - Url = "https://media.discordapp.net/attachments/279231165045407744/1037396920001372240/unknown.png?ex=65709bab&is=655e26ab&hm=8329f7f07e635c9493c12f53054d090908b8643d0932313d49f2b2ff79601a36&=&format=webp", - Credit = "https://discordapp.com/users/Planewalker#5903", - IdFormat = "D2", - StartIndex = 1, - Count = 5 } ]); public static readonly Location GateOfTorment = new( diff --git a/Daybreak/Services/Updater/ApplicationUpdater.cs b/Daybreak/Services/Updater/ApplicationUpdater.cs index 198620f8..98e2618a 100644 --- a/Daybreak/Services/Updater/ApplicationUpdater.cs +++ b/Daybreak/Services/Updater/ApplicationUpdater.cs @@ -15,6 +15,7 @@ using System.Data; using System.Diagnostics; using System.Extensions; +using System.Extensions.Core; using System.IO; using System.Net.Http; using System.Net.Http.Json; @@ -68,6 +69,7 @@ internal sealed class ApplicationUpdater( public async Task DownloadUpdate(Version version, UpdateStatus updateStatus) { + var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: version.ToString()); if (version.HasPrefix is false) { version.HasPrefix = true; @@ -78,21 +80,29 @@ public async Task DownloadUpdate(Version version, UpdateStatus updateStatu return await this.DownloadUpdateInternalLegacy(version, updateStatus); } - var maybeMetadataResponse = await this.httpClient.GetAsync( + try + { + var maybeMetadataResponse = await this.httpClient.GetAsync( BlobStorageUrl .Replace(VersionTag, version.ToString().Replace(".", "-")) .Replace(FileTag, "Metadata.json")); - if (maybeMetadataResponse.IsSuccessStatusCode) - { - var metaData = await maybeMetadataResponse.Content.ReadFromJsonAsync>(); - if (metaData is not null) + if (maybeMetadataResponse.IsSuccessStatusCode) { - return await - await new TaskFactory().StartNew(() => this.DownloadUpdateInternalBlob(metaData, version, updateStatus), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Current); + var metaData = await maybeMetadataResponse.Content.ReadFromJsonAsync>(); + if (metaData is not null) + { + return await + await new TaskFactory().StartNew(() => this.DownloadUpdateInternalBlob(metaData, version, updateStatus), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Current); + } } - } - return await this.DownloadUpdateInternalLegacy(version, updateStatus); + return await this.DownloadUpdateInternalLegacy(version, updateStatus); + } + catch (Exception e) + { + scopedLogger.LogError(e, "Failed to download update for version {version}", version); + return false; + } } public async Task DownloadLatestUpdate(UpdateStatus updateStatus) @@ -122,31 +132,52 @@ public async Task UpdateAvailable() public async Task> GetVersions() { - this.logger.LogDebug($"Retrieving version list from {VersionListUrl}"); - var response = await this.httpClient.GetAsync(VersionListUrl); - if (response.IsSuccessStatusCode) + var scopedLogger = this.logger.CreateScopedLogger(); + scopedLogger.LogDebug($"Retrieving version list from {VersionListUrl}"); + try { - var serializedList = await response.Content.ReadAsStringAsync(); - var versionList = serializedList.Deserialize(); - return versionList!.Select(v => v.Ref![RefTagPrefix.Length..]).Select(v => new Version(v)); - } + var response = await this.httpClient.GetAsync(VersionListUrl); + if (response.IsSuccessStatusCode) + { + var serializedList = await response.Content.ReadAsStringAsync(); + var versionList = serializedList.Deserialize(); + return versionList!.Select(v => v.Ref![RefTagPrefix.Length..]).Select(v => new Version(v)); + } - return []; + scopedLogger.LogError("Failed to retrieve version list. Status code: {statusCode}", response.StatusCode); + return []; + } + catch (Exception e) + { + scopedLogger.LogError(e, "Failed to retrieve version list from {url}", VersionListUrl); + return []; + } } public async Task GetChangelog(Version version) { - var changeLogResponse = await this.httpClient.GetAsync( + var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: version.ToString()); + try + { + var changeLogResponse = await this.httpClient.GetAsync( BlobStorageUrl .Replace(VersionTag, version.ToString().Replace(".", "-")) .Replace(FileTag, "changelog.txt")); - if (!changeLogResponse.IsSuccessStatusCode) + if (!changeLogResponse.IsSuccessStatusCode) + { + scopedLogger.LogError("Failed to retrieve changelog for version {version}. Status code: {statusCode}", version, changeLogResponse.StatusCode); + return default; + } + + scopedLogger.LogDebug("Retrieved changelog for version {version}", version); + return await changeLogResponse.Content.ReadAsStringAsync(); + } + catch (Exception e) { + scopedLogger.LogError(e, "Failed to retrieve changelog for version {version}", version); return default; } - - return await changeLogResponse.Content.ReadAsStringAsync(); } public void PeriodicallyCheckForUpdates() @@ -198,7 +229,7 @@ public void OnClosing() private async Task DownloadUpdateInternalBlob(List metadata, Version version, UpdateStatus updateStatus) { - var scopedLogger = this.logger.CreateScopedLogger(nameof(this.DownloadUpdateInternalBlob), version.ToString()); + var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: version.ToString()); updateStatus.CurrentStep = DownloadStatus.InitializingDownload; // Exclude daybreak packed files @@ -341,34 +372,46 @@ private async Task DownloadUpdateInternalBlob(List metadata, Ver private async Task DownloadUpdateInternalLegacy(Version version, UpdateStatus updateStatus) { + var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: version.VersionString); updateStatus.CurrentStep = DownloadStatus.InitializingDownload; var uri = DownloadUrl.Replace(VersionTag, version.ToString()); if (await this.downloadService.DownloadFile(uri, TempFile, updateStatus) is false) { - this.logger.LogError("Failed to download update file"); + scopedLogger.LogError("Failed to download update file"); return false; } updateStatus.CurrentStep = UpdateStatus.PendingRestart; - this.logger.LogDebug("Downloaded update file"); + scopedLogger.LogDebug("Downloaded update file"); return true; } private async Task GetLatestVersion() { - using var response = await this.httpClient.GetAsync(Url); - if (response.IsSuccessStatusCode) + var scopedLogger = this.logger.CreateScopedLogger(); + try { - var versionTag = response.RequestMessage!.RequestUri!.ToString().Split('/').Last().TrimStart('v'); - if (Version.TryParse(versionTag, out var parsedVersion)) + using var response = await this.httpClient.GetAsync(Url); + if (response.IsSuccessStatusCode) { - return parsedVersion; + var versionTag = response.RequestMessage!.RequestUri!.ToString().Split('/').Last().TrimStart('v'); + if (Version.TryParse(versionTag, out var parsedVersion)) + { + return parsedVersion; + } + + scopedLogger.LogError("Failed to parse version from {versionTag}", versionTag); + return default; } + scopedLogger.LogError("Failed to retrieve latest version. Status code: {statusCode}", response.StatusCode); + return default; + } + catch(Exception e) + { + scopedLogger.LogError(e, "Failed to retrieve latest version from {url}", Url); return default; } - - return default; } private void LaunchExtractor() diff --git a/Daybreak/Views/MetricsView.xaml.cs b/Daybreak/Views/MetricsView.xaml.cs index e6d9055f..8781efc7 100644 --- a/Daybreak/Views/MetricsView.xaml.cs +++ b/Daybreak/Views/MetricsView.xaml.cs @@ -1,5 +1,7 @@ using Daybreak.Shared.Models.Metrics; using Daybreak.Shared.Services.Metrics; +using LiveChartsCore.Defaults; +using LiveChartsCore.Kernel; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.VisualElements; @@ -91,6 +93,7 @@ private void UserControl_Unloaded(object sender, RoutedEventArgs e) private void CartesianChart_Loaded(object sender, RoutedEventArgs e) { var cartesianChart = sender.Cast(); + cartesianChart.CoreChart.Update(new ChartUpdateParams { IsAutomaticUpdate = false, Throttling = false }); if (cartesianChart.DataContext is not MetricSetViewModel metricSet) { return; @@ -112,22 +115,11 @@ metricSet.Metrics is null || cartesianChart.DrawMargin = new LiveChartsCore.Measure.Margin(30); cartesianChart.XAxes = [ - new Axis + new DateTimeAxis(TimeSpan.FromSeconds(1), dateTime => dateTime.ToString("d")) { Name = "Time", - Labeler = (ticks) => - { - return new DateTime((long)ticks).ToString("HH:mm:ss"); - }, LabelsPaint = this.foregroundPaint, - SeparatorsPaint = this.transparentPaint, - CrosshairLabelsPaint = this.transparentPaint, - CrosshairPaint = this.transparentPaint, NamePaint = this.foregroundPaint, - SubseparatorsPaint = this.transparentPaint, - SubticksPaint = this.transparentPaint, - TicksPaint = this.transparentPaint, - ZeroPaint = this.transparentPaint, } ]; @@ -137,27 +129,20 @@ metricSet.Metrics is null || { Name = metricSet.Instrument.Unit, LabelsPaint = this.foregroundPaint, - SeparatorsPaint = this.transparentPaint, - CrosshairLabelsPaint = this.transparentPaint, - CrosshairPaint = this.transparentPaint, NamePaint = this.foregroundPaint, - SubseparatorsPaint = this.transparentPaint, - SubticksPaint = this.transparentPaint, - TicksPaint = this.transparentPaint, - ZeroPaint = this.transparentPaint, } ]; cartesianChart.Series = [ - new LineSeries + new LineSeries { Values = metricSet.AggregationType switch { - AggregationTypes.NoAggregate => PlotNoAggregation(metricSet.Metrics), - AggregationTypes.P95 => PlotPercentageAggregation(metricSet.Metrics, 0.95), - AggregationTypes.P98 => PlotPercentageAggregation(metricSet.Metrics, 0.98), - AggregationTypes.P99 => PlotPercentageAggregation(metricSet.Metrics, 0.99), + AggregationTypes.NoAggregate => ToDateTimePoint(PlotNoAggregation(metricSet.Metrics)), + AggregationTypes.P95 => ToDateTimePoint(PlotPercentageAggregation(metricSet.Metrics, 0.95)), + AggregationTypes.P98 => ToDateTimePoint(PlotPercentageAggregation(metricSet.Metrics, 0.98)), + AggregationTypes.P99 => ToDateTimePoint(PlotPercentageAggregation(metricSet.Metrics, 0.99)), _ => throw new InvalidOperationException("Unable to plot metrics. Unknown aggregation") }, Fill = default, @@ -198,4 +183,9 @@ private static IReadOnlyCollection PlotPercentageAggregation(ObservableC return [.. finalDataSet]; } + + private static List ToDateTimePoint(IEnumerable dataSet) + { + return [.. dataSet.Select(s => new DateTimePoint(s.Timestamp, System.Convert.ToDouble(s.Measurement)))]; + } } diff --git a/Daybreak/Views/Trade/PriceHistoryView.xaml b/Daybreak/Views/Trade/PriceHistoryView.xaml index 0f99249d..f3edb4f7 100644 --- a/Daybreak/Views/Trade/PriceHistoryView.xaml +++ b/Daybreak/Views/Trade/PriceHistoryView.xaml @@ -1,61 +1,42 @@ - - - - - + + + - - - + ToolTip="Go back to price quotes" /> + + diff --git a/Daybreak/Views/Trade/PriceHistoryView.xaml.cs b/Daybreak/Views/Trade/PriceHistoryView.xaml.cs index 3e9cf197..10d013cb 100644 --- a/Daybreak/Views/Trade/PriceHistoryView.xaml.cs +++ b/Daybreak/Views/Trade/PriceHistoryView.xaml.cs @@ -3,6 +3,9 @@ using Daybreak.Shared.Services.Navigation; using Daybreak.Shared.Services.TradeChat; using LiveChartsCore; +using LiveChartsCore.Defaults; +using LiveChartsCore.Drawing; +using LiveChartsCore.Measure; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.VisualElements; @@ -45,6 +48,9 @@ public partial class PriceHistoryView : UserControl [GenerateDependencyProperty] private DateTime endDateTime = DateTime.MaxValue; + [GenerateDependencyProperty] + private Margin drawMargin = new(float.NaN, float.NaN, float.NaN, 300); + [GenerateDependencyProperty] private bool loading = false; @@ -62,6 +68,7 @@ public PriceHistoryView( this.foregroundPaint = new SolidColorPaint(new SKColor(skForegroundBrush.Color.R, skForegroundBrush.Color.G, skForegroundBrush.Color.B, skForegroundBrush.Color.A)); this.accentPaint = new SolidColorPaint(new SKColor(skAccentBrush.Color.R, skAccentBrush.Color.G, skAccentBrush.Color.B, skAccentBrush.Color.A), 2); this.backgroundPaint = new SolidColorPaint(new SKColor(skBackgroundBrush.Color.R, skBackgroundBrush.Color.G, skBackgroundBrush.Color.B, skBackgroundBrush.Color.A)); + this.PopulateChart(); } private void UserControl_DataContextChanged(object _, DependencyPropertyChangedEventArgs __) @@ -74,18 +81,6 @@ private void BackButton_Clicked(object sender, EventArgs e) this.viewManager.ShowView(); } - private void HighlightButton_Clicked(object sender, EventArgs e) - { - var xAxis = this.XAxes.FirstOrDefault(); - if (xAxis is null) - { - return; - } - - xAxis.MinLimit = this.StartDateTime.Ticks; - xAxis.MaxLimit = this.EndDateTime.Ticks; - } - private async void FetchPriceHistory() { if (this.DataContext is not ItemBase itemBase) @@ -115,33 +110,15 @@ private async void FetchPriceHistory() private void PopulateChart() { - this.EndDateTime = this.traderQuotes.OrderByDescending(t => t.Timestamp).FirstOrDefault()?.Timestamp ?? DateTime.Now; - this.StartDateTime = this.EndDateTime - TimeSpan.FromDays(1); - this.XAxes = [ - new Axis + new DateTimeAxis(TimeSpan.FromHours(1), dateTime => dateTime.ToString("d")) { Name = "Date", LabelsPaint = this.foregroundPaint, - Labeler = ticks => - { - if (double.IsNaN(ticks)) - { - return string.Empty; - } - - var t = (long)Math.Round(ticks); - if (t < DateTime.MinValue.Ticks || t > DateTime.MaxValue.Ticks) - { - return string.Empty; - } - - return new DateTime(t, DateTimeKind.Utc).ToString("d"); - }, - MinLimit = this.StartDateTime.Ticks, - MaxLimit = this.EndDateTime.Ticks, - MinStep = TimeSpan.FromHours(1).Ticks, + NameTextSize = 16, + NamePaint = this.foregroundPaint, + InLineNamePlacement = true } ]; @@ -150,24 +127,25 @@ private void PopulateChart() new Axis { Name = "Price", + NameTextSize = 16, + NamePadding = new Padding(0, 15), + NamePaint = this.foregroundPaint, LabelsPaint = this.foregroundPaint, - Labeler = (price) => $"{price*20d:0}g", + Labeler = Labelers.Currency, + InLineNamePlacement = true } ]; this.Series = [ - new LineSeries + new StepLineSeries { - Values = this.traderQuotes, - Fill = default, - IsHoverable = false, + Values = [.. ProcessQuotes(this.traderQuotes).Select(t => new DateTimePoint(t.Timestamp ?? DateTime.MinValue, t.Price))], Stroke = this.accentPaint, - LineSmoothness = 0, + Name = "Historical Price", GeometryStroke = default, GeometryFill = default, GeometrySize = default, - Name = string.Empty, } ]; @@ -175,7 +153,20 @@ private void PopulateChart() { Text = $"{this.traderQuotes.FirstOrDefault()?.Item?.Name} Price Chart", TextSize = 22, - Paint = this.foregroundPaint, + Paint = this.foregroundPaint }; } + + private static IEnumerable ProcessQuotes(IEnumerable quotes) + { + var lastPricePoint = double.MinValue; + foreach(var quote in quotes.OrderBy(p => p.Timestamp)) + { + if (quote.Price != lastPricePoint) + { + lastPricePoint = quote.Price; + yield return quote; + } + } + } } diff --git a/Directory.Build.props b/Directory.Build.props index 4af462bc..17b2d1f3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ $(NoWarn);IL2104;IL3053;IL3000;IL3002;NU1701;CS0108 - 0.9.9.77 + 0.9.9.78 enable enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 4378b925..f591436b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,78 +1,74 @@ - + true - - - - - - - - - - - - - - - - - - - - + + - + + + - + + + + + - - - - + + + + + + + - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + - - - + + + + - - - - - - - - + + + + + + - + \ No newline at end of file