diff --git a/SpeedrunTracker/Models/RunDetails.cs b/SpeedrunTracker/Models/RunDetails.cs index 3658bd9..74ab1d4 100644 --- a/SpeedrunTracker/Models/RunDetails.cs +++ b/SpeedrunTracker/Models/RunDetails.cs @@ -4,9 +4,9 @@ public class RunDetails { public int Place { get; set; } public required Speedrun Run { get; set; } - public required Category Category { get; set; } + public Category? Category { get; set; } public Level? Level { get; set; } - public required GamePlatform Platform { get; set; } + public GamePlatform? Platform { get; set; } public required List Variables { get; set; } public GameAssets? GameAssets { get; set; } public Ruleset? Ruleset { get; set; } diff --git a/SpeedrunTracker/Models/SpeedrunDotCom/LeaderboardEntry.cs b/SpeedrunTracker/Models/SpeedrunDotCom/LeaderboardEntry.cs index d716a79..0d8957c 100644 --- a/SpeedrunTracker/Models/SpeedrunDotCom/LeaderboardEntry.cs +++ b/SpeedrunTracker/Models/SpeedrunDotCom/LeaderboardEntry.cs @@ -7,43 +7,47 @@ public record LeaderboardEntry { public int Place { get; set; } public required Speedrun Run { get; set; } - public BaseData Game { get; set; } + public BaseData? Game { get; set; } // For some ungodly reason it's a single object when there is a level and an empty list if there's not. [JsonPropertyName("level")] - public BaseData LevelJson { get; set; } + public BaseData? LevelJson { get; set; } - public BaseData Category { get; set; } + public BaseData? Category { get; set; } // For some ungodly reason it's a single object when there is a platform and an empty list if there's not. [JsonPropertyName("platform")] - public BaseData PlatformJson { get; set; } + public BaseData? PlatformJson { get; set; } - private Level _level; + private Level? _level; [JsonIgnore] - public Level Level + public Level? Level { get { - _level ??= Run.LevelId == null ? null : JsonSerializer.Deserialize(LevelJson.Data.ToString()); + _level ??= Run.LevelId == null || LevelJson?.Data == null + ? null + : JsonSerializer.Deserialize(LevelJson.Data.ToString()!); return _level; } } - private GamePlatform _platform; + private GamePlatform? _platform; [JsonIgnore] - public GamePlatform Platform + public GamePlatform? Platform { get { - _platform ??= Run?.System?.PlatformId == null ? null : JsonSerializer.Deserialize(PlatformJson.Data.ToString()); + _platform ??= Run?.System?.PlatformId == null || PlatformJson?.Data == null + ? null + : JsonSerializer.Deserialize(PlatformJson.Data.ToString()!); return _platform; } } - private string _ordinalPlace; + private string? _ordinalPlace; [JsonIgnore] public string OrdinalPlace @@ -55,10 +59,10 @@ public string OrdinalPlace } } - private Asset _trophyAsset; + private Asset? _trophyAsset; [JsonIgnore] - public Asset TrophyAsset + public Asset? TrophyAsset { get { diff --git a/SpeedrunTracker/Models/SpeedrunDotCom/User.cs b/SpeedrunTracker/Models/SpeedrunDotCom/User.cs index 4bd48ae..3e24a86 100644 --- a/SpeedrunTracker/Models/SpeedrunDotCom/User.cs +++ b/SpeedrunTracker/Models/SpeedrunDotCom/User.cs @@ -30,7 +30,16 @@ public record User : BaseSpeedrunObject public UserAssets? Assets { get; set; } [JsonIgnore] - public string? DisplayName => PlayerType == PlayerType.Guest ? Name : string.IsNullOrEmpty(Names?.International) ? Names?.Japanese : Names?.International; + public string? DisplayName + { + get + { + if (PlayerType == PlayerType.Guest) + return Name; + + return string.IsNullOrEmpty(Names?.International) ? Names?.Japanese : Names.International; + } + } public static User GetUserNotFoundPlaceholder() { diff --git a/SpeedrunTracker/Services/DialogService.cs b/SpeedrunTracker/Services/DialogService.cs index 3d488c2..f0586d5 100644 --- a/SpeedrunTracker/Services/DialogService.cs +++ b/SpeedrunTracker/Services/DialogService.cs @@ -12,8 +12,8 @@ public Task ShowAlertAsync(string title, string message, string cancel = "OK") public async Task ShowConfirmationAsync(string title, string message, string accept = "Yes", string cancel = "No") { Page? mainPage = GetMainPage(); - return mainPage != null ? await mainPage.DisplayAlert(title, message, accept, cancel) : false; + return mainPage != null && await mainPage.DisplayAlert(title, message, accept, cancel); } - private Page? GetMainPage() => Application.Current?.Windows[0].Page; + private static Page? GetMainPage() => Application.Current?.Windows[0].Page; } diff --git a/SpeedrunTracker/Services/EmbedService.cs b/SpeedrunTracker/Services/EmbedService.cs index d7fcc5b..2a5e565 100644 --- a/SpeedrunTracker/Services/EmbedService.cs +++ b/SpeedrunTracker/Services/EmbedService.cs @@ -28,7 +28,7 @@ private EmbeddableUrl HandleYouTube(string url) { Uri uri = new(url); NameValueCollection query = HttpUtility.ParseQueryString(uri.Query); - string? videoId = query.AllKeys.Contains("v") == true ? query["v"] : uri.Segments[^1]; + string? videoId = query.AllKeys.Contains("v") ? query["v"] : uri.Segments[^1]; return new EmbeddableUrl() { Url = url, diff --git a/SpeedrunTracker/ViewModels/GameDetailViewModel.cs b/SpeedrunTracker/ViewModels/GameDetailViewModel.cs index 916675f..3f5aa24 100644 --- a/SpeedrunTracker/ViewModels/GameDetailViewModel.cs +++ b/SpeedrunTracker/ViewModels/GameDetailViewModel.cs @@ -12,9 +12,9 @@ public class GameDetailViewModel : BaseFollowViewModel private readonly ILeaderboardService _leaderboardService; private readonly IUserService _userService; private readonly ILocalSettingsService _settingsService; - private IEnumerable _fullGameCategories; - private IEnumerable _levelCategories; - private IEnumerable _allVariables; + private IEnumerable? _fullGameCategories; + private IEnumerable? _levelCategories; + private IEnumerable? _allVariables; private int _leaderboardEntriesVisible; private const int _leaderboardEntriesStepSize = 10; @@ -27,7 +27,7 @@ public GameDetailViewModel(IGameService gameService, ILeaderboardService leaderb LeaderboardEntries = new RangedObservableCollection(); } - public Game Game + public Game? Game { get => _followEntity; set @@ -38,16 +38,16 @@ public Game Game } } - private ObservableCollection _categories; + private ObservableCollection? _categories; public ObservableCollection Categories { - get => _categories; + get => _categories ?? []; set { - if (_categories.SequenceEqualOrNull(value)) + if (_categories != null && _categories.SequenceEqualOrNull(value)) { - if (_allVariables.Any(x => x.Scope.Type == VariableScopeType.SingleLevel)) + if (_allVariables != null && _allVariables.Any(x => x.Scope.Type == VariableScopeType.SingleLevel)) UpdateVariables(); return; } @@ -58,9 +58,9 @@ public ObservableCollection Categories } } - private Category _selectedCategory; + private Category? _selectedCategory; - public Category SelectedCategory + public Category? SelectedCategory { get => _selectedCategory; set @@ -75,14 +75,14 @@ public Category SelectedCategory } } - private ObservableCollection _levels; + private ObservableCollection? _levels; public ObservableCollection Levels { - get => _levels; + get => _levels ?? []; set { - if (_levels.SequenceEqualOrNull(value)) + if (_levels == null || _levels.SequenceEqualOrNull(value)) return; _levels = value; @@ -92,9 +92,9 @@ public ObservableCollection Levels } } - private Level _selectedLevel; + private Level? _selectedLevel; - public Level SelectedLevel + public Level? SelectedLevel { get => _selectedLevel; set @@ -104,18 +104,18 @@ public Level SelectedLevel _selectedLevel = value; NotifyPropertyChanged(); - Categories = string.IsNullOrEmpty(value.Id) ? _fullGameCategories.AsObservableCollection() : _levelCategories.AsObservableCollection(); + Categories = string.IsNullOrEmpty(value?.Id) ? (_fullGameCategories ?? []).AsObservableCollection() : (_levelCategories ?? []).AsObservableCollection(); } } - private ObservableCollection _variables; + private ObservableCollection? _variables; public ObservableCollection Variables { - get => _variables; + get => _variables ?? []; set { - if (_variables.SequenceEqualOrNull(value)) + if (_variables == null || _variables.SequenceEqualOrNull(value)) return; _variables = value; @@ -123,7 +123,7 @@ public ObservableCollection Variables } } - private Leaderboard _leaderboard; + private Leaderboard? _leaderboard; public RangedObservableCollection LeaderboardEntries { get; set; } private bool _isLoadingLeaderboard; @@ -143,9 +143,9 @@ public bool IsLoadingLeaderboard public bool HasIndividualLevels => _levels != null && _levels.Count > 1; - private LeaderboardEntry _selectedLeaderboardEntry; + private LeaderboardEntry? _selectedLeaderboardEntry; - public LeaderboardEntry SelectedLeaderboardEntry + public LeaderboardEntry? SelectedLeaderboardEntry { get => _selectedLeaderboardEntry; set @@ -179,7 +179,10 @@ public string Platforms public async Task LoadCategoriesAsync() { - List categories = await ExecuteNetworkTask(_gameService.GetGameCategoriesAsync(Game.Id)); + if (Game == null) + return false; + + List? categories = await ExecuteNetworkTask(_gameService.GetGameCategoriesAsync(Game.Id)); if (categories == null) return false; @@ -190,8 +193,11 @@ public async Task LoadCategoriesAsync() public async Task LoadLevelsAsync() { + if (Game == null) + return false; + List allLevels = new() { new() { Name = "Full Game" } }; - List gameLevels = await ExecuteNetworkTask(_gameService.GetGameLevelsAsync(Game.Id)); + List? gameLevels = await ExecuteNetworkTask(_gameService.GetGameLevelsAsync(Game.Id)); if (gameLevels == null) return false; @@ -202,12 +208,18 @@ public async Task LoadLevelsAsync() public async Task LoadVariablesAsync() { + if (Game == null) + return false; + _allVariables = await ExecuteNetworkTask(_gameService.GetGameVariablesAsync(Game.Id)); return _allVariables != null; } public async Task LoadLeaderboardAsync() { + if (Game == null) + return; + IsLoadingLeaderboard = true; _leaderboardEntriesVisible = 0; _leaderboard = null; @@ -219,7 +231,7 @@ public async Task LoadLeaderboardAsync() if (Variables != null) { foreach (VariableViewModel vm in Variables) - variableValues.Add($"var-{vm.VariableId}={vm.SelectedValue.Id}"); + variableValues.Add($"var-{vm.VariableId}={vm.SelectedValue?.Id}"); variables = string.Join('&', variableValues); } @@ -228,7 +240,7 @@ public async Task LoadLeaderboardAsync() if (!string.IsNullOrEmpty(variables)) variables = $"&{variables}"; - _leaderboard = string.IsNullOrEmpty(SelectedLevel.Id) ? + _leaderboard = string.IsNullOrEmpty(SelectedLevel?.Id) ? await ExecuteNetworkTask(_leaderboardService.GetFullGameLeaderboardAsync(Game.Id, SelectedCategory.Id, variables, _settingsService.UserSettings.MaxLeaderboardResults)) : await ExecuteNetworkTask(_leaderboardService.GetLevelLeaderboardAsync(Game.Id, SelectedLevel.Id, SelectedCategory.Id, variables, _settingsService.UserSettings.MaxLeaderboardResults)); @@ -250,8 +262,9 @@ private void DisplayLeaderboardEntries() for (int i = 0; i < entry.Run.Players.Count; i++) { if (entry.Run.Players[i].PlayerType == PlayerType.User) - entry.Run.Players[i] = _leaderboard.Players.Data.Find(x => x.Id == entry.Run.Players[i].Id); - entry.TrophyAsset = Game.Assets.GetTrophyAsset(entry.Place); + entry.Run.Players[i] = _leaderboard.Players.Data.First(x => x.Id == entry.Run.Players[i].Id); + + entry.TrophyAsset = Game?.Assets.GetTrophyAsset(entry.Place); } } @@ -261,22 +274,25 @@ private void DisplayLeaderboardEntries() private async Task NavigateToRunAsync() { - if (_selectedLeaderboardEntry == null) + if (_selectedLeaderboardEntry == null || Game == null) + return; + + Category? category = _categories?.FirstOrDefault(x => x.Id == _selectedLeaderboardEntry.Run.CategoryId); + if (category == null) return; - Category category = _categories.FirstOrDefault(x => x.Id == _selectedLeaderboardEntry.Run.CategoryId); - Level level = _levels.FirstOrDefault(x => x.Id == _selectedLeaderboardEntry.Run.LevelId); - GamePlatform platform = Game.Platforms.Data.Find(x => x.Id == _selectedLeaderboardEntry.Run.System.PlatformId); + Level? level = _levels?.FirstOrDefault(x => x.Id == _selectedLeaderboardEntry.Run.LevelId); + GamePlatform? platform = Game.Platforms.Data.Find(x => x.Id == _selectedLeaderboardEntry.Run.System?.PlatformId); - User examiner = null; - string examinerId = _selectedLeaderboardEntry.Run.Status.ExaminerId; + User? examiner = null; + string? examinerId = _selectedLeaderboardEntry.Run.Status?.ExaminerId; if (examinerId != null) examiner = Game.Moderators.Data.Find(x => x.Id == examinerId) ?? await ExecuteNetworkTask(_userService.GetUserAsync(examinerId)) ?? User.GetUserNotFoundPlaceholder(); if (!_selectedLeaderboardEntry.Run.Variables.Any()) foreach (KeyValuePair valuePair in _selectedLeaderboardEntry.Run.Values) { - Variable variable = _allVariables.FirstOrDefault(x => x.Id == valuePair.Key); + Variable? variable = _allVariables?.FirstOrDefault(x => x.Id == valuePair.Key); if (variable == null) continue; @@ -307,13 +323,13 @@ private async Task NavigateToRunAsync() private void UpdateVariables() { List variablesVMs = new(); - IEnumerable variables = _allVariables.Where(x => x.IsSubcategory); - if (string.IsNullOrEmpty(SelectedLevel.Id)) + IEnumerable variables = (_allVariables ?? []).Where(x => x.IsSubcategory); + if (string.IsNullOrEmpty(_selectedLevel?.Id)) variables = variables.Where(x => x.Scope.Type == VariableScopeType.Global || x.Scope.Type == VariableScopeType.FullGame); else - variables = variables.Where(x => x.Scope.Type == VariableScopeType.Global || x.Scope.Type == VariableScopeType.AllLevels || x.Scope.Type == VariableScopeType.SingleLevel && x.Scope.Level == SelectedLevel.Id); + variables = variables.Where(x => x.Scope.Type == VariableScopeType.Global || x.Scope.Type == VariableScopeType.AllLevels || x.Scope.Type == VariableScopeType.SingleLevel && x.Scope.Level == _selectedLevel?.Id); - foreach (Variable variable in variables.Where(x => x.Category == null || x.Category == SelectedCategory.Id)) + foreach (Variable variable in variables.Where(x => x.Category == null || x.Category == _selectedCategory?.Id)) { VariableViewModel vm = new() { diff --git a/SpeedrunTracker/ViewModels/RunDetailsViewModel.cs b/SpeedrunTracker/ViewModels/RunDetailsViewModel.cs index 98c2d33..9930bcf 100644 --- a/SpeedrunTracker/ViewModels/RunDetailsViewModel.cs +++ b/SpeedrunTracker/ViewModels/RunDetailsViewModel.cs @@ -99,7 +99,7 @@ public EmbeddableUrl? SelectedVideo public ICommand OpenLinkCommand => new AsyncRelayCommand(OpenLinkAsync); - public string? Title => RunDetails == null ? "RunDetails" : $"{_runDetails?.Category.Name} in {_runDetails?.Run?.Times?.PrimaryTimeSpan} by {_runDetails?.Run.Players[0].DisplayName}"; + public string? Title => RunDetails == null ? "RunDetails" : $"{_runDetails?.Category?.Name} in {_runDetails?.Run?.Times?.PrimaryTimeSpan} by {_runDetails?.Run.Players[0].DisplayName}"; public string? FormattedDate => RunDetails?.Run.Date?.ToString(_settingsService.UserSettings.DateFormat); @@ -151,7 +151,7 @@ public RunDetailsViewModel(IBrowserService browserService, IUserService userServ private async Task ShowVideo() { if (!string.IsNullOrEmpty(_selectedVideo?.Url)) - await _browserService.OpenAsync(_selectedVideo.Url); + await _browserService.OpenAsync(_selectedVideo.Url); } diff --git a/SpeedrunTracker/ViewModels/UserDetailsViewModel.cs b/SpeedrunTracker/ViewModels/UserDetailsViewModel.cs index 3ded25e..91c5c1c 100644 --- a/SpeedrunTracker/ViewModels/UserDetailsViewModel.cs +++ b/SpeedrunTracker/ViewModels/UserDetailsViewModel.cs @@ -116,7 +116,9 @@ private async Task LoadPersonalBests(bool showLevels) await ParseRunPlayersAsync(entry, players); } - groups.Add(new UserPersonalBestsGroup(entries[0].Game.Data, entries.Select(x => new UserRunViewModel(x, _settingsService.UserSettings.DateFormat)))); + BaseGame? game = entries[0].Game?.Data; + if (game != null) + groups.Add(new UserPersonalBestsGroup(game, entries.Select(x => new UserRunViewModel(x, _settingsService.UserSettings.DateFormat)))); } PersonalBests = groups.AsObservableCollection(); @@ -127,11 +129,11 @@ private async Task LoadPersonalBests(bool showLevels) } } - private void ParseRunVariables(LeaderboardEntry entry) + private static void ParseRunVariables(LeaderboardEntry entry) { foreach (KeyValuePair valuePair in entry.Run.Values) { - Variable? variable = entry.Category.Data.Variables.Data.Find(x => x.Id == valuePair.Key); + Variable? variable = entry.Category?.Data.Variables.Data.Find(x => x.Id == valuePair.Key); if (variable == null || !variable.Values.Values.ContainsKey(valuePair.Value)) continue; @@ -172,8 +174,8 @@ private async Task NavigateToRun() RunDetails runDetails = new() { - Category = leaderboardEntry.Category.Data, - GameAssets = leaderboardEntry.Game.Data.Assets, + Category = leaderboardEntry.Category?.Data, + GameAssets = leaderboardEntry.Game?.Data.Assets, Examiner = examiner, Level = leaderboardEntry.Level, Place = leaderboardEntry.Place, diff --git a/SpeedrunTracker/Views/GameDetailsPage.xaml.cs b/SpeedrunTracker/Views/GameDetailsPage.xaml.cs index 44729b5..4a1a250 100644 --- a/SpeedrunTracker/Views/GameDetailsPage.xaml.cs +++ b/SpeedrunTracker/Views/GameDetailsPage.xaml.cs @@ -8,7 +8,7 @@ public partial class GameDetailPage : BaseDetailPage private readonly GameDetailViewModel _viewModel; private bool _isLoaded; - public Game Game + public Game? Game { get => _viewModel.Game; set => _viewModel.Game = value; @@ -55,7 +55,7 @@ private async void ContentPage_Appearing(object sender, EventArgs e) } } - private async Task NagivateBack() + private static async Task NagivateBack() { await Shell.Current.Navigation.PopAsync(); }