From 577a31d00f00318272875cb063c0b997e2076f53 Mon Sep 17 00:00:00 2001 From: dend <1389609+dend@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:33:27 -0800 Subject: [PATCH] QOL improvements and fixes - Fixes bug where data-based logs were done even if logging is disabled. - Adds the ability to see medal details when tapped on one of them in the medal view. - Adds a query that can find all matches based on medal ID. - Rudimentary support for matches browsing from medal view --- .../Controls/MatchesGridControl.xaml | 200 ++++++++++++++++++ .../Controls/MatchesGridControl.xaml.cs | 64 ++++++ src/OpenSpartan.Workshop/Data/DataHandler.cs | 163 ++++++++------ .../Data/MedalMatchesSource.cs | 38 ++++ src/OpenSpartan.Workshop/MainWindow.xaml.cs | 11 +- .../OpenSpartan.Workshop.csproj | 11 + .../OpenSpartan.Workshop.csproj.user | 6 + .../Select/PlayerMatchesBasedOnMedal.sql | 99 +++++++++ .../Shared/RelayCommand.cs | 38 ++++ .../Shared/UserContextManager.cs | 32 +++ .../ViewModels/MedalMatchesViewModel.cs | 52 +++++ .../ViewModels/MedalsViewModel.cs | 17 +- src/OpenSpartan.Workshop/Views/HomeView.xaml | 16 +- .../Views/MatchesView.xaml | 176 +-------------- .../Views/MedalMatchesView.xaml | 34 +++ .../Views/MedalMatchesView.xaml.cs | 35 +++ .../Views/MedalsView.xaml | 13 +- .../Views/MedalsView.xaml.cs | 46 ++++ 18 files changed, 788 insertions(+), 263 deletions(-) create mode 100644 src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml create mode 100644 src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml.cs create mode 100644 src/OpenSpartan.Workshop/Data/MedalMatchesSource.cs create mode 100644 src/OpenSpartan.Workshop/Queries/Select/PlayerMatchesBasedOnMedal.sql create mode 100644 src/OpenSpartan.Workshop/Shared/RelayCommand.cs create mode 100644 src/OpenSpartan.Workshop/ViewModels/MedalMatchesViewModel.cs create mode 100644 src/OpenSpartan.Workshop/Views/MedalMatchesView.xaml create mode 100644 src/OpenSpartan.Workshop/Views/MedalMatchesView.xaml.cs diff --git a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml new file mode 100644 index 0000000..b2cd50e --- /dev/null +++ b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml.cs b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml.cs new file mode 100644 index 0000000..20263c2 --- /dev/null +++ b/src/OpenSpartan.Workshop/Controls/MatchesGridControl.xaml.cs @@ -0,0 +1,64 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using CommunityToolkit.WinUI.UI.Controls; +using System.Collections; + +namespace OpenSpartan.Workshop.Controls +{ + public sealed partial class MatchesGridControl : UserControl + { + public static readonly DependencyProperty MatchSourceProperty = + DependencyProperty.Register( + "MatchSource", // Name of the property + typeof(IEnumerable), // Type of the property + typeof(MatchesGridControl), // Type of the owner (your user control) + new PropertyMetadata(null) // Optional metadata, e.g., default value + ); + + public IEnumerable MatchSource + { + get { return (IEnumerable)GetValue(MatchSourceProperty); } + set { SetValue(MatchSourceProperty, value); } + } + + public MatchesGridControl() + { + this.InitializeComponent(); + } + + private void dgdMatches_PointerReleased(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e) + { + DataGridRow row = FindParent((UIElement)e.OriginalSource); + + if (row != null) + { + if (row.DetailsVisibility == Visibility.Visible) + { + row.DetailsVisibility = Visibility.Collapsed; + } + else + { + row.DetailsVisibility = Visibility.Visible; + } + } + } + + public static T FindParent(DependencyObject childElement) where T : Control + { + DependencyObject currentElement = childElement; + + while (currentElement != null) + { + if (currentElement is T matchingElement) + { + return matchingElement; + } + + currentElement = VisualTreeHelper.GetParent(currentElement); + } + + return null; + } + } +} diff --git a/src/OpenSpartan.Workshop/Data/DataHandler.cs b/src/OpenSpartan.Workshop/Data/DataHandler.cs index b88e261..dedb873 100644 --- a/src/OpenSpartan.Workshop/Data/DataHandler.cs +++ b/src/OpenSpartan.Workshop/Data/DataHandler.cs @@ -53,12 +53,12 @@ internal static string SetWALJournalingMode() } else { - Logger.Error($"WAL journaling mode not set."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"WAL journaling mode not set."); } } catch (Exception ex) { - Logger.Error($"Journaling mode modification exception: {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"Journaling mode modification exception: {ex.Message}"); } return null; @@ -91,7 +91,7 @@ internal static bool BootstrapDatabase() } catch (Exception ex) { - Logger.Error($"Database bootstrapping failure: {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"Database bootstrapping failure: {ex.Message}"); return false; } } @@ -119,11 +119,11 @@ private static void SetupIndices(SqliteConnection connection) if (outcome > 0) { - Logger.Info("Indices provisioned."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info("Indices provisioned."); } else { - Logger.Warn("Indices could not be set up. If this is not the first run, then those are likely already configured."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Warn("Indices could not be set up. If this is not the first run, then those are likely already configured."); } } @@ -151,7 +151,7 @@ internal static bool InsertServiceRecordEntry(string serviceRecordJson) } catch (Exception ex) { - Logger.Error($"Error inserting service record entry. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"Error inserting service record entry. {ex.Message}"); return false; } } @@ -179,12 +179,12 @@ internal static List GetMatchIds() } else { - Logger.Warn($"No rows returned for distinct match IDs."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Warn($"No rows returned for distinct match IDs."); } } catch (Exception ex) { - Logger.Error($"An error occurred obtaining unique match IDs. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"An error occurred obtaining unique match IDs. {ex.Message}"); } return null; @@ -215,18 +215,28 @@ internal static RewardTrackMetadata GetOperationResponseBody(string operationPat } else { - Logger.Warn($"No rows returned for operations."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Warn($"No rows returned for operations."); } } catch (Exception ex) { - Logger.Error($"An error occurred obtaining operations from database. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"An error occurred obtaining operations from database. {ex.Message}"); } return null; } internal static List GetMatches(string playerXuid, string boundaryTime, int boundaryLimit) + { + return GetMatchesInternal(playerXuid, null, boundaryTime, boundaryLimit); + } + + internal static List GetMatchesWithMedal(string playerXuid, long medalNameId, string boundaryTime, int boundaryLimit) + { + return GetMatchesInternal(playerXuid, medalNameId, boundaryTime, boundaryLimit); + } + + private static List GetMatchesInternal(string playerXuid, long? medalNameId, string boundaryTime, int boundaryLimit) { try { @@ -234,7 +244,16 @@ internal static List GetMatches(string playerXuid, string boun connection.Open(); using var command = connection.CreateCommand(); - command.CommandText = GetQuery("Select", "PlayerMatches"); + if (medalNameId.HasValue) + { + command.CommandText = GetQuery("Select", "PlayerMatchesBasedOnMedal"); + command.Parameters.AddWithValue("MedalNameId", medalNameId.Value); + } + else + { + command.CommandText = GetQuery("Select", "PlayerMatches"); + } + command.Parameters.AddWithValue("PlayerXuid", playerXuid); command.Parameters.AddWithValue("BoundaryTime", boundaryTime); command.Parameters.AddWithValue("BoundaryLimit", boundaryLimit); @@ -242,64 +261,68 @@ internal static List GetMatches(string playerXuid, string boun using var reader = command.ExecuteReader(); if (reader.HasRows) { - List matches = new(); + List matches = new List(); while (reader.Read()) { - var matchOrdinal = reader.GetOrdinal("MatchId"); - var startTimeOrdinal = reader.GetOrdinal("StartTime"); - var rankOrdinal = reader.GetOrdinal("Rank"); - var outcomeOrdinal = reader.GetOrdinal("Outcome"); - var gameVariantCategoryOrdinal = reader.GetOrdinal("GameVariantCategory"); - var mapOrdinal = reader.GetOrdinal("Map"); - var playlistOrdinal = reader.GetOrdinal("Playlist"); - var gameVariantOrdinal = reader.GetOrdinal("GameVariant"); - var durationOrdinal = reader.GetOrdinal("Duration"); - var lastTeamIdOrdinal = reader.GetOrdinal("LastTeamId"); - var teamsOrdinal = reader.GetOrdinal("Teams"); - var participationInfoOrdinal = reader.GetOrdinal("ParticipationInfo"); - var playerTeamStatsOrdinal = reader.GetOrdinal("PlayerTeamStats"); - var teamMmrOrdinal = reader.GetOrdinal("TeamMmr"); - var expectedDeathsOrdinal = reader.GetOrdinal("ExpectedDeaths"); - var expectedKillsOrdinal = reader.GetOrdinal("ExpectedKills"); - - MatchTableEntity entity = new() - { - MatchId = reader.IsDBNull(matchOrdinal) ? string.Empty : reader.GetFieldValue(matchOrdinal), - StartTime = reader.IsDBNull(startTimeOrdinal) ? DateTimeOffset.UnixEpoch : reader.GetFieldValue(startTimeOrdinal).ToLocalTime(), - Rank = reader.IsDBNull(rankOrdinal) ? 0 : reader.GetFieldValue(rankOrdinal), - Outcome = reader.IsDBNull(outcomeOrdinal) ? Outcome.DidNotFinish : reader.GetFieldValue(outcomeOrdinal), - Category = reader.IsDBNull(gameVariantCategoryOrdinal) ? GameVariantCategory.None : reader.GetFieldValue(gameVariantCategoryOrdinal), - Map = reader.IsDBNull(mapOrdinal) ? string.Empty : reader.GetFieldValue(mapOrdinal), - Playlist = reader.IsDBNull(playlistOrdinal) ? string.Empty : reader.GetFieldValue(playlistOrdinal), - GameVariant = reader.IsDBNull(gameVariantOrdinal) ? string.Empty : reader.GetFieldValue(gameVariantOrdinal), - Duration = reader.IsDBNull(durationOrdinal) ? TimeSpan.Zero : XmlConvert.ToTimeSpan(reader.GetFieldValue(durationOrdinal)), - LastTeamId = reader.IsDBNull(durationOrdinal) ? null : reader.GetFieldValue(lastTeamIdOrdinal), - Teams = reader.IsDBNull(teamsOrdinal) ? null : JsonSerializer.Deserialize>(reader.GetFieldValue(teamsOrdinal), serializerOptions), - ParticipationInfo = reader.IsDBNull(teamsOrdinal) ? null : JsonSerializer.Deserialize(reader.GetFieldValue(participationInfoOrdinal), serializerOptions), - PlayerTeamStats = reader.IsDBNull(teamsOrdinal) ? null : JsonSerializer.Deserialize>(reader.GetFieldValue(playerTeamStatsOrdinal), serializerOptions), - TeamMmr = reader.IsDBNull(teamMmrOrdinal) ? null : reader.GetFieldValue(teamMmrOrdinal), - ExpectedDeaths = reader.IsDBNull(expectedDeathsOrdinal) ? null : reader.GetFieldValue(expectedDeathsOrdinal), - ExpectedKills = reader.IsDBNull(expectedKillsOrdinal) ? null : reader.GetFieldValue(expectedKillsOrdinal), - }; - - matches.Add(entity); + matches.Add(ReadMatchTableEntity(reader)); } return matches; } else { - Logger.Warn($"No rows returned for player match IDs."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Warn($"No rows returned for player match IDs."); } } catch (Exception ex) { - Logger.Error($"An error occurred obtaining matches. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"An error occurred obtaining matches. {ex.Message}"); } return null; } + private static MatchTableEntity ReadMatchTableEntity(SqliteDataReader reader) + { + var matchOrdinal = reader.GetOrdinal("MatchId"); + var startTimeOrdinal = reader.GetOrdinal("StartTime"); + var rankOrdinal = reader.GetOrdinal("Rank"); + var outcomeOrdinal = reader.GetOrdinal("Outcome"); + var gameVariantCategoryOrdinal = reader.GetOrdinal("GameVariantCategory"); + var mapOrdinal = reader.GetOrdinal("Map"); + var playlistOrdinal = reader.GetOrdinal("Playlist"); + var gameVariantOrdinal = reader.GetOrdinal("GameVariant"); + var durationOrdinal = reader.GetOrdinal("Duration"); + var lastTeamIdOrdinal = reader.GetOrdinal("LastTeamId"); + var teamsOrdinal = reader.GetOrdinal("Teams"); + var participationInfoOrdinal = reader.GetOrdinal("ParticipationInfo"); + var playerTeamStatsOrdinal = reader.GetOrdinal("PlayerTeamStats"); + var teamMmrOrdinal = reader.GetOrdinal("TeamMmr"); + var expectedDeathsOrdinal = reader.GetOrdinal("ExpectedDeaths"); + var expectedKillsOrdinal = reader.GetOrdinal("ExpectedKills"); + + return new MatchTableEntity + { + MatchId = reader.IsDBNull(matchOrdinal) ? string.Empty : reader.GetFieldValue(matchOrdinal), + StartTime = reader.IsDBNull(startTimeOrdinal) ? DateTimeOffset.UnixEpoch : reader.GetFieldValue(startTimeOrdinal).ToLocalTime(), + Rank = reader.IsDBNull(rankOrdinal) ? 0 : reader.GetFieldValue(rankOrdinal), + Outcome = reader.IsDBNull(outcomeOrdinal) ? Outcome.DidNotFinish : reader.GetFieldValue(outcomeOrdinal), + Category = reader.IsDBNull(gameVariantCategoryOrdinal) ? GameVariantCategory.None : reader.GetFieldValue(gameVariantCategoryOrdinal), + Map = reader.IsDBNull(mapOrdinal) ? string.Empty : reader.GetFieldValue(mapOrdinal), + Playlist = reader.IsDBNull(playlistOrdinal) ? string.Empty : reader.GetFieldValue(playlistOrdinal), + GameVariant = reader.IsDBNull(gameVariantOrdinal) ? string.Empty : reader.GetFieldValue(gameVariantOrdinal), + Duration = reader.IsDBNull(durationOrdinal) ? TimeSpan.Zero : XmlConvert.ToTimeSpan(reader.GetFieldValue(durationOrdinal)), + LastTeamId = reader.IsDBNull(durationOrdinal) ? null : reader.GetFieldValue(lastTeamIdOrdinal), + Teams = reader.IsDBNull(teamsOrdinal) ? null : JsonSerializer.Deserialize>(reader.GetFieldValue(teamsOrdinal), serializerOptions), + ParticipationInfo = reader.IsDBNull(teamsOrdinal) ? null : JsonSerializer.Deserialize(reader.GetFieldValue(participationInfoOrdinal), serializerOptions), + PlayerTeamStats = reader.IsDBNull(teamsOrdinal) ? null : JsonSerializer.Deserialize>(reader.GetFieldValue(playerTeamStatsOrdinal), serializerOptions), + TeamMmr = reader.IsDBNull(teamMmrOrdinal) ? null : reader.GetFieldValue(teamMmrOrdinal), + ExpectedDeaths = reader.IsDBNull(expectedDeathsOrdinal) ? null : reader.GetFieldValue(expectedDeathsOrdinal), + ExpectedKills = reader.IsDBNull(expectedKillsOrdinal) ? null : reader.GetFieldValue(expectedKillsOrdinal), + }; + } + + internal static Tuple GetMatchStatsAvailability(string matchId) { try @@ -328,7 +351,7 @@ internal static Tuple GetMatchStatsAvailability(string matchId) } catch (Exception ex) { - Logger.Error($"An error occurred obtaining match and stats availability. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"An error occurred obtaining match and stats availability. {ex.Message}"); } return null; @@ -355,7 +378,7 @@ internal static bool InsertPlayerMatchStats(string matchId, string statsBody) } catch (Exception ex) { - Logger.Error($"An error occurred inserting player match and stats. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"An error occurred inserting player match and stats. {ex.Message}"); } return false; @@ -381,7 +404,7 @@ internal static bool InsertMatchStats (string matchBody) } catch (Exception ex) { - Logger.Error($"An error occurred inserting match and stats. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"An error occurred inserting match and stats. {ex.Message}"); } return false; @@ -447,7 +470,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) if (insertionResult > 0) { - Logger.Info($"Stored map: {result.MatchInfo.MapVariant.AssetId}/{result.MatchInfo.MapVariant.VersionId}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"Stored map: {result.MatchInfo.MapVariant.AssetId}/{result.MatchInfo.MapVariant.VersionId}"); } } } @@ -466,7 +489,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) if (insertionResult > 0) { - Logger.Info($"Stored playlist: {result.MatchInfo.Playlist.AssetId}/{result.MatchInfo.Playlist.VersionId}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"Stored playlist: {result.MatchInfo.Playlist.AssetId}/{result.MatchInfo.Playlist.VersionId}"); } } } @@ -485,7 +508,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) if (insertionResult > 0) { - Logger.Info($"Stored playlist + map mode pair: {result.MatchInfo.PlaylistMapModePair.AssetId}/{result.MatchInfo.PlaylistMapModePair.VersionId}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"Stored playlist + map mode pair: {result.MatchInfo.PlaylistMapModePair.AssetId}/{result.MatchInfo.PlaylistMapModePair.VersionId}"); } } } @@ -507,7 +530,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) if (insertionResult > 0) { - Logger.Info($"Stored game variant: {result.MatchInfo.UgcGameVariant.AssetId}/{result.MatchInfo.UgcGameVariant.VersionId}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"Stored game variant: {result.MatchInfo.UgcGameVariant.AssetId}/{result.MatchInfo.UgcGameVariant.VersionId}"); } } @@ -539,7 +562,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) if (insertionResult > 0) { - Logger.Info($"Stored engine game variant: {engineGameVariant.Result.AssetId}/{engineGameVariant.Result.VersionId}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"Stored engine game variant: {engineGameVariant.Result.AssetId}/{engineGameVariant.Result.VersionId}"); } } } @@ -548,7 +571,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result) } catch (Exception ex) { - Logger.Error($"Error updating match stats. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"Error updating match stats. {ex.Message}"); return false; } } @@ -571,7 +594,7 @@ internal static List GetMedals() using var reader = command.ExecuteReader(); if (reader.HasRows) { - List matchIds = new(); + List matchIds = []; while (reader.Read()) { matchIds.AddRange(JsonSerializer.Deserialize>(reader.GetString(0))); @@ -581,12 +604,12 @@ internal static List GetMedals() } else { - Logger.Warn($"No rows returned for medals."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Warn($"No rows returned for medals."); } } catch (Exception ex) { - Logger.Error($"An error occurred obtaining medals from the database. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"An error occurred obtaining medals from the database. {ex.Message}"); } return null; @@ -608,7 +631,7 @@ internal static bool UpdateOperationRewardTracks(string response, string path) if (insertionResult > 0) { - Logger.Info($"Stored reward track {path}."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"Stored reward track {path}."); return true; } else @@ -633,7 +656,7 @@ internal static bool UpdateInventoryItems(string response, string path) if (insertionResult > 0) { - Logger.Info($"Stored inventory item {path}."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"Stored inventory item {path}."); return true; } else @@ -721,12 +744,12 @@ internal static InGameItem GetInventoryItem(string path) } else { - Logger.Info($"No rows returned for inventory items query."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"No rows returned for inventory items query."); } } catch (Exception ex) { - Logger.Error($"An error occurred obtaining inventory items. {ex.Message}"); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"An error occurred obtaining inventory items. {ex.Message}"); } return null; @@ -752,11 +775,11 @@ internal static bool InsertOwnedInventoryItems(PlayerInventory result) var insertionResult = insertionCommand.ExecuteNonQuery(); if (insertionResult > 0) { - Logger.Info($"Stored owned inventory item {item.ItemId}."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Info($"Stored owned inventory item {item.ItemId}."); } else { - Logger.Error($"Could not store owned inventory item {item.ItemId}."); + if (SettingsViewModel.Instance.EnableLogging) Logger.Error($"Could not store owned inventory item {item.ItemId}."); } } diff --git a/src/OpenSpartan.Workshop/Data/MedalMatchesSource.cs b/src/OpenSpartan.Workshop/Data/MedalMatchesSource.cs new file mode 100644 index 0000000..8996ae5 --- /dev/null +++ b/src/OpenSpartan.Workshop/Data/MedalMatchesSource.cs @@ -0,0 +1,38 @@ +using CommunityToolkit.Common.Collections; +using OpenSpartan.Workshop.Models; +using OpenSpartan.Workshop.Shared; +using OpenSpartan.Workshop.ViewModels; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenSpartan.Workshop.Data +{ + public class MedalMatchesSource : IIncrementalSource + { + public MedalMatchesSource() + { + Task.Run(() => + { + UserContextManager.GetPlayerMatches(); + }); + } + + Task> IIncrementalSource.GetPagedItemsAsync(int pageIndex, int pageSize, CancellationToken cancellationToken) + { + if (MedalMatchesViewModel.Instance.MatchList != null && MedalMatchesViewModel.Instance.MatchList.Count > 0) + { + var date = MedalMatchesViewModel.Instance.MatchList.Min(a => a.StartTime).ToString("o", CultureInfo.InvariantCulture); + var matches = Task.Run(() => (IEnumerable)DataHandler.GetMatchesWithMedal($"xuid({HomeViewModel.Instance.Xuid})", MedalMatchesViewModel.Instance.Medal.NameId, date, pageSize)); + + return matches; + } + else + { + return null; + } + } + } +} diff --git a/src/OpenSpartan.Workshop/MainWindow.xaml.cs b/src/OpenSpartan.Workshop/MainWindow.xaml.cs index 50c9847..82df4dc 100644 --- a/src/OpenSpartan.Workshop/MainWindow.xaml.cs +++ b/src/OpenSpartan.Workshop/MainWindow.xaml.cs @@ -2,6 +2,8 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Navigation; +using OpenSpartan.Workshop.ViewModels; +using OpenSpartan.Workshop.Views; using System; using System.Linq; @@ -34,9 +36,12 @@ private void On_Navigated(object sender, NavigationEventArgs e) else if (ContentFrame.SourcePageType != null) { // Select the nav view item that corresponds to the page being navigated to. - nvRoot.SelectedItem = nvRoot.MenuItems - .OfType() - .First(i => i.Tag.Equals(ContentFrame.SourcePageType.FullName.ToString())); + if (ContentFrame.SourcePageType != typeof(MedalMatchesView)) + { + nvRoot.SelectedItem = nvRoot.MenuItems + .OfType() + .First(i => i.Tag.Equals(ContentFrame.SourcePageType.FullName.ToString())); + } } } diff --git a/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj b/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj index 25931ef..487bb83 100644 --- a/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj +++ b/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj @@ -14,12 +14,14 @@ true + + @@ -63,6 +65,9 @@ + + MSBuild:Compile + Always @@ -158,9 +163,15 @@ Always + + Always + Always + + MSBuild:Compile + MSBuild:Compile diff --git a/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj.user b/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj.user index 63ea68b..42188ad 100644 --- a/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj.user +++ b/src/OpenSpartan.Workshop/OpenSpartan.Workshop.csproj.user @@ -23,6 +23,12 @@ Designer + + Designer + + + Designer + Designer diff --git a/src/OpenSpartan.Workshop/Queries/Select/PlayerMatchesBasedOnMedal.sql b/src/OpenSpartan.Workshop/Queries/Select/PlayerMatchesBasedOnMedal.sql new file mode 100644 index 0000000..3c2a521 --- /dev/null +++ b/src/OpenSpartan.Workshop/Queries/Select/PlayerMatchesBasedOnMedal.sql @@ -0,0 +1,99 @@ +WITH RAW_MATCHES AS ( + SELECT + MS.MatchId, + MS.Teams, + json_extract(MS.MatchInfo, '$.StartTime') AS StartTime, + json_extract(MS.MatchInfo, '$.Duration') AS Duration, + json_extract(MS.MatchInfo, '$.GameVariantCategory') AS GameVariantCategory, + json_extract(MS.MatchInfo, '$.MapVariant.AssetId') AS Map, + json_extract(MS.MatchInfo, '$.Playlist.AssetId') AS Playlist, + json_extract(MS.MatchInfo, '$.UgcGameVariant.AssetId') AS UgcGameVariant, + json_extract(value, '$.Rank') AS "Rank", + json_extract(value, '$.Outcome') AS Outcome, + json_extract(value, '$.LastTeamId') AS LastTeamId, + json_extract(value, '$.ParticipationInfo') AS ParticipationInfo, + json_extract(value, '$.PlayerTeamStats') AS PlayerTeamStats + FROM + MatchStats MS + JOIN + json_each(MS.Players) PE ON json_extract(PE.value, '$.PlayerId') = $PlayerXuid + WHERE + MS.MatchInfo IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM json_each(PlayerTeamStats) AS PTS + WHERE EXISTS ( + SELECT 1 + FROM json_each(PTS.value, '$.Stats.CoreStats.Medals') AS MedalEntry + WHERE json_extract(MedalEntry.value, '$.NameId') = $MedalNameId + ) + ) + ORDER BY + StartTime DESC +), +MATCH_DETAILS AS ( + SELECT + PMS.MatchId, + json_extract(PE.value, '$.Result.TeamMmr') AS TeamMmr, + json_extract(PE.value, '$.Result.Counterfactuals.SelfCounterfactuals.Deaths') AS ExpectedDeaths, + json_extract(PE.value, '$.Result.Counterfactuals.SelfCounterfactuals.Kills') AS ExpectedKills + FROM + PlayerMatchStats PMS + JOIN + json_each(PMS.PlayerStats) PE ON json_extract(PE.value, '$.Id') = $PlayerXuid + WHERE + PMS.PlayerStats IS NOT NULL +), +SELECTIVE_MATCHES AS ( + SELECT + MatchId, + Teams, + StartTime, + Duration, + "Rank", + Outcome, + LastTeamId, + ParticipationInfo, + PlayerTeamStats, + GameVariantCategory, + Map, + Playlist, + UgcGameVariant + FROM + RAW_MATCHES + WHERE + StartTime <= $BoundaryTime + LIMIT + $BoundaryLimit +) +SELECT + SM.MatchId, + SM.Teams, + SM.StartTime, + SM.Duration, + SM."Rank", + SM.Outcome, + SM.LastTeamId, + SM.ParticipationInfo, + SM.PlayerTeamStats, + SM.GameVariantCategory, + M.PublicName AS Map, + P.PublicName AS Playlist, + GV.PublicName AS GameVariant, + MD.TeamMmr AS TeamMmr, + MD.ExpectedDeaths AS ExpectedDeaths, + MD.ExpectedKills AS ExpectedKills +FROM + SELECTIVE_MATCHES SM +LEFT JOIN + Maps M ON M.AssetId = SM.Map +LEFT JOIN + Playlists P ON P.AssetId = SM.Playlist +LEFT JOIN + GameVariants GV ON GV.AssetId = SM.UgcGameVariant +LEFT JOIN + MATCH_DETAILS MD ON MD.MatchId = SM.MatchId +GROUP BY + SM.MatchId +ORDER BY + StartTime DESC; diff --git a/src/OpenSpartan.Workshop/Shared/RelayCommand.cs b/src/OpenSpartan.Workshop/Shared/RelayCommand.cs new file mode 100644 index 0000000..864c156 --- /dev/null +++ b/src/OpenSpartan.Workshop/Shared/RelayCommand.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace OpenSpartan.Workshop.Shared +{ + public class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func _canExecute; + + public event EventHandler CanExecuteChanged; + + public RelayCommand(Action execute, Func canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public bool CanExecute(object parameter) + { + return _canExecute == null || _canExecute((T)parameter); + } + + public void Execute(object parameter) + { + _execute((T)parameter); + } + + public void RaiseCanExecuteChanged() + { + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/OpenSpartan.Workshop/Shared/UserContextManager.cs b/src/OpenSpartan.Workshop/Shared/UserContextManager.cs index 1525a13..aa99725 100644 --- a/src/OpenSpartan.Workshop/Shared/UserContextManager.cs +++ b/src/OpenSpartan.Workshop/Shared/UserContextManager.cs @@ -704,6 +704,38 @@ await dispatcherWindow.DispatcherQueue.EnqueueAsync(() => } } + internal static async void PopulateMedalMatchData(long medalNameId) + { + if (HomeViewModel.Instance.Xuid != null) + { + List matches = null; + string date = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture); + + if (MedalMatchesViewModel.Instance.MatchList.Count == 0) + { + matches = DataHandler.GetMatchesWithMedal($"xuid({HomeViewModel.Instance.Xuid})", medalNameId, date, 100); + } + else + { + date = MedalMatchesViewModel.Instance.MatchList.Min(a => a.StartTime).ToString("o", CultureInfo.InvariantCulture); + matches = DataHandler.GetMatchesWithMedal($"xuid({HomeViewModel.Instance.Xuid})", medalNameId, date, 10); + } + + if (matches != null) + { + var dispatcherWindow = ((Application.Current as App)?.MainWindow) as MainWindow; + await dispatcherWindow.DispatcherQueue.EnqueueAsync(() => + { + MedalMatchesViewModel.Instance.MatchList.AddRange(matches); + }); + } + else + { + if (SettingsViewModel.Instance.EnableLogging) Logger.Error("Could not get the list of matches for the specified parameters."); + } + } + } + internal static async Task PopulateMedalData() { try diff --git a/src/OpenSpartan.Workshop/ViewModels/MedalMatchesViewModel.cs b/src/OpenSpartan.Workshop/ViewModels/MedalMatchesViewModel.cs new file mode 100644 index 0000000..4986688 --- /dev/null +++ b/src/OpenSpartan.Workshop/ViewModels/MedalMatchesViewModel.cs @@ -0,0 +1,52 @@ +using CommunityToolkit.WinUI; +using Den.Dev.Orion.Models.HaloInfinite; +using OpenSpartan.Workshop.Data; +using OpenSpartan.Workshop.Models; +using OpenSpartan.Workshop.Shared; +using System.Runtime.CompilerServices; + +namespace OpenSpartan.Workshop.ViewModels +{ + internal class MedalMatchesViewModel : Observable + { + public static MedalMatchesViewModel Instance { get; } = new MedalMatchesViewModel(); + + private IncrementalLoadingCollection _matchList; + private Medal _medal; + + public MedalMatchesViewModel() + { + MatchList = new IncrementalLoadingCollection(); + } + + public Medal Medal + { + get => _medal; + set + { + if (_medal != value) + { + _medal = value; + NotifyPropertyChanged(); + } + } + } + public IncrementalLoadingCollection MatchList + { + get => _matchList; + set + { + if (_matchList != value) + { + _matchList = value; + NotifyPropertyChanged(); + } + } + } + + public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) + { + OnPropertyChanged(propertyName); + } + } +} diff --git a/src/OpenSpartan.Workshop/ViewModels/MedalsViewModel.cs b/src/OpenSpartan.Workshop/ViewModels/MedalsViewModel.cs index 7e6be87..7da08c0 100644 --- a/src/OpenSpartan.Workshop/ViewModels/MedalsViewModel.cs +++ b/src/OpenSpartan.Workshop/ViewModels/MedalsViewModel.cs @@ -1,5 +1,8 @@ using Den.Dev.Orion.Models.HaloInfinite; +using Microsoft.UI.Xaml.Controls; using OpenSpartan.Workshop.Shared; +using OpenSpartan.Workshop.Views; +using System; using System.Collections.ObjectModel; using System.Linq; using System.Runtime.CompilerServices; @@ -10,10 +13,17 @@ internal class MedalsViewModel : Observable { public static MedalsViewModel Instance { get; } = new MedalsViewModel(); - private MedalsViewModel() { } + private MedalsViewModel() + { + NavigateCommand = new RelayCommand(NavigateToAnotherView); + } private ObservableCollection> _medals; + public RelayCommand NavigateCommand { get; } + + public event EventHandler NavigationRequested; + public ObservableCollection> Medals { get => _medals; @@ -27,6 +37,11 @@ public ObservableCollection> Medals } } + private void NavigateToAnotherView(long parameter) + { + NavigationRequested?.Invoke(this, parameter); + } + public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { OnPropertyChanged(propertyName); diff --git a/src/OpenSpartan.Workshop/Views/HomeView.xaml b/src/OpenSpartan.Workshop/Views/HomeView.xaml index d2f1d80..2fdcd71 100644 --- a/src/OpenSpartan.Workshop/Views/HomeView.xaml +++ b/src/OpenSpartan.Workshop/Views/HomeView.xaml @@ -14,18 +14,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/OpenSpartan.Workshop/Views/MedalMatchesView.xaml b/src/OpenSpartan.Workshop/Views/MedalMatchesView.xaml new file mode 100644 index 0000000..0af45ab --- /dev/null +++ b/src/OpenSpartan.Workshop/Views/MedalMatchesView.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenSpartan.Workshop/Views/MedalMatchesView.xaml.cs b/src/OpenSpartan.Workshop/Views/MedalMatchesView.xaml.cs new file mode 100644 index 0000000..33e8fa5 --- /dev/null +++ b/src/OpenSpartan.Workshop/Views/MedalMatchesView.xaml.cs @@ -0,0 +1,35 @@ +using System.Linq; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; +using OpenSpartan.Workshop.Shared; +using OpenSpartan.Workshop.ViewModels; + +namespace OpenSpartan.Workshop.Views +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MedalMatchesView : Page + { + public MedalMatchesView() + { + this.InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + // Access the parameter using QueryParameter + if (e.Parameter != null && e.Parameter is long parameter) + { + // Make sure to reset the match list. + MedalMatchesViewModel.Instance.MatchList = new CommunityToolkit.WinUI.IncrementalLoadingCollection(); + + UserContextManager.PopulateMedalMatchData(parameter); + + MedalMatchesViewModel.Instance.Medal = MedalsViewModel.Instance.Medals.SelectMany(group => group).FirstOrDefault(i => i.NameId == parameter); + } + } + } +} diff --git a/src/OpenSpartan.Workshop/Views/MedalsView.xaml b/src/OpenSpartan.Workshop/Views/MedalsView.xaml index d6aacb4..7c55f35 100644 --- a/src/OpenSpartan.Workshop/Views/MedalsView.xaml +++ b/src/OpenSpartan.Workshop/Views/MedalsView.xaml @@ -6,6 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" + x:Name="MedalsViewPage" xmlns:converters="using:OpenSpartan.Workshop.Converters" xmlns:viewmodels="using:OpenSpartan.Workshop.ViewModels" DataContext="{x:Bind viewmodels:MedalsViewModel.Instance}" @@ -43,7 +44,7 @@ - + @@ -66,6 +67,16 @@ + + + diff --git a/src/OpenSpartan.Workshop/Views/MedalsView.xaml.cs b/src/OpenSpartan.Workshop/Views/MedalsView.xaml.cs index 8be8e14..48e7d88 100644 --- a/src/OpenSpartan.Workshop/Views/MedalsView.xaml.cs +++ b/src/OpenSpartan.Workshop/Views/MedalsView.xaml.cs @@ -1,5 +1,8 @@ +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; using OpenSpartan.Workshop.Shared; +using OpenSpartan.Workshop.ViewModels; namespace OpenSpartan.Workshop.Views { @@ -8,6 +11,17 @@ public sealed partial class MedalsView : Page public MedalsView() { InitializeComponent(); + this.Loaded += MedalsView_Loaded; + } + + private void MedalsView_Loaded(object sender, RoutedEventArgs e) + { + ((MedalsViewModel)this.DataContext).NavigationRequested += ViewModel_NavigationRequested; + } + + private void ViewModel_NavigationRequested(object sender, long e) + { + Frame.Navigate(typeof(MedalMatchesView), e); } private async void btnRefreshMedals_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) @@ -15,5 +29,37 @@ private async void btnRefreshMedals_Click(object sender, Microsoft.UI.Xaml.Route await UserContextManager.PopulateServiceRecordData(); await UserContextManager.PopulateMedalData(); } + + private void MedalGridItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e) + { + var teachingTip = FindChildElement((DependencyObject)sender); + if (teachingTip != null) + { + teachingTip.IsOpen = true; + } + } + + public static T FindChildElement(DependencyObject parent) where T : FrameworkElement + { + if (parent == null) + return null; + + int childCount = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < childCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(parent, i); + + if (child is T typedChild) + { + return typedChild; + } + + T result = FindChildElement(child); + if (result != null) + return result; + } + + return null; + } } }