diff --git a/Stardrop/Models/Data/Enums/ChangelogState.cs b/Stardrop/Models/Data/Enums/ChangelogState.cs new file mode 100644 index 0000000..7d54715 --- /dev/null +++ b/Stardrop/Models/Data/Enums/ChangelogState.cs @@ -0,0 +1,8 @@ +namespace Stardrop.Models.Data.Enums +{ + public enum ChangelogState + { + Unknown, + Fetching + } +} diff --git a/Stardrop/Models/Mod.cs b/Stardrop/Models/Mod.cs index 562fec4..d0ca570 100644 --- a/Stardrop/Models/Mod.cs +++ b/Stardrop/Models/Mod.cs @@ -69,7 +69,7 @@ public bool IsEnabled public string ChangeStateText { get { return IsEnabled ? Program.translation.Get("internal.disable") : Program.translation.Get("internal.enable"); } } public string ChangeWholeModGroupStateText { get { return IsEnabled ? Program.translation.Get("internal.disable_whole_mod") : Program.translation.Get("internal.enable_whole_mod"); } } private WikiCompatibilityStatus _status { get; set; } - public WikiCompatibilityStatus Status { get { return _status; } set { _status = value; NotifyPropertyChanged("Status"); NotifyPropertyChanged("ParsedStatus"); NotifyPropertyChanged("InstallStatus"); } } + public WikiCompatibilityStatus Status { get { return _status; } set { _status = value; NotifyPropertyChanged("Status"); NotifyPropertyChanged("ParsedStatus"); NotifyPropertyChanged("InstallStatus"); NotifyPropertyChanged("HasChangelog"); } } public string ParsedStatus { get @@ -115,6 +115,17 @@ public string InstallStatus } } + private ChangelogState _changelogState { get; set; } + public ChangelogState ChangelogState { get { return _changelogState; } set { _changelogState = value; NotifyPropertyChanged("ChangelogState"); } } + + public bool HasChangelog + { + get + { + return !String.IsNullOrEmpty(SuggestedVersion) && IsModOutdated(SuggestedVersion) && GetNexusId() is not null; + } + } + private string _note { get; set; } public string Note { get { return _note; } set { _note = value; NotifyPropertyChanged("Note"); } } diff --git a/Stardrop/Models/Nexus/Web/ChangelogVersion.cs b/Stardrop/Models/Nexus/Web/ChangelogVersion.cs new file mode 100644 index 0000000..245cee0 --- /dev/null +++ b/Stardrop/Models/Nexus/Web/ChangelogVersion.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace Stardrop.Models.Nexus.Web +{ + public record ChangelogVersion(string Version, List Changes); +} diff --git a/Stardrop/Utilities/External/NexusClient.cs b/Stardrop/Utilities/External/NexusClient.cs index f54f189..55bb99d 100644 --- a/Stardrop/Utilities/External/NexusClient.cs +++ b/Stardrop/Utilities/External/NexusClient.cs @@ -544,6 +544,56 @@ public async Task SetModEndorsement(int modId, bool isEndor return EndorsementResponse.Unknown; } + /// + /// Gets every published changelog for the given mod, keyed by version string. + /// Returns null if the request fails; an empty dictionary means the mod publishes no changelogs. + /// + public async Task>?> GetModChangelogs(int modId) + { + try + { + var response = await _client.GetAsync($"games/stardewvalley/mods/{modId}/changelogs.json"); + if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null) + { + string content = await response.Content.ReadAsStringAsync(); + var changelogs = JsonSerializer.Deserialize>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (changelogs is null) + { + Program.helper.Log($"Unable to get the changelogs for the mod {modId} on Nexus Mods"); + Program.helper.Log($"Response from Nexus Mods:\n{content}"); + + return null; + } + + UpdateRequestCounts(response.Headers); + + return changelogs; + } + else + { + if (response.StatusCode != System.Net.HttpStatusCode.OK) + { + Program.helper.Log($"Bad status given from Nexus Mods: {response.StatusCode}"); + if (response.Content is not null) + { + Program.helper.Log($"Response from Nexus Mods:\n{await response.Content.ReadAsStringAsync()}"); + } + } + else if (response.Content is null) + { + Program.helper.Log($"No response from Nexus Mods!"); + } + } + } + catch (Exception ex) + { + Program.helper.Log($"Unable to get the changelogs for the mod {modId} on Nexus Mods: {ex}", Helper.Status.Alert); + } + + return null; + } + public async Task DownloadThumbnail(int modId) { try diff --git a/Stardrop/ViewModels/ChangelogWindowViewModel.cs b/Stardrop/ViewModels/ChangelogWindowViewModel.cs new file mode 100644 index 0000000..9332616 --- /dev/null +++ b/Stardrop/ViewModels/ChangelogWindowViewModel.cs @@ -0,0 +1,81 @@ +using ReactiveUI; +using Semver; +using Stardrop.Models.Nexus.Web; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; + +namespace Stardrop.ViewModels +{ + public class ChangelogWindowViewModel : ViewModelBase + { + public ObservableCollection Versions { get; set; } + + private string _modName = String.Empty; + public string ModName { get { return _modName; } set { this.RaiseAndSetIfChanged(ref _modName, value); } } + + private bool _hasChanges; + public bool HasChanges { get { return _hasChanges; } set { this.RaiseAndSetIfChanged(ref _hasChanges, value); } } + + private bool _isLoading; + public bool IsLoading { get { return _isLoading; } set { this.RaiseAndSetIfChanged(ref _isLoading, value); } } + + private bool _showEmptyMessage; + public bool ShowEmptyMessage { get { return _showEmptyMessage; } set { this.RaiseAndSetIfChanged(ref _showEmptyMessage, value); } } + + private string _modPageUri = String.Empty; + public string ModPageUri { get { return _modPageUri; } set { this.RaiseAndSetIfChanged(ref _modPageUri, value); } } + + private bool _hasModPage; + public bool HasModPage { get { return _hasModPage; } set { this.RaiseAndSetIfChanged(ref _hasModPage, value); } } + + private string _emptyMessage = String.Empty; + public string EmptyMessage { get { return _emptyMessage; } set { this.RaiseAndSetIfChanged(ref _emptyMessage, value); } } + + public ChangelogWindowViewModel() + { + Versions = new ObservableCollection(); + } + + /// + /// Orders changelogs newest first. The keys can't be relied on for ordering - they arrive in + /// dictionary order and sort incorrectly as strings ("2.8.9" would land above "2.8.35"). + /// Entries are HTML-decoded as Nexus stores them encoded. + /// + public static List SortNewestFirst(Dictionary> changelogs) + { + var parsed = new List<(SemVersion Version, ChangelogVersion Entry)>(); + var unparsed = new List(); + + foreach (var changelog in changelogs) + { + var entry = new ChangelogVersion(changelog.Key, changelog.Value.ConvertAll(c => WebUtility.HtmlDecode(c) ?? c)); + + if (SemVersion.TryParse(StripVersionPrefix(changelog.Key), SemVersionStyles.Any, out var version)) + { + parsed.Add((version, entry)); + } + else + { + unparsed.Add(entry); + } + } + + parsed.Sort((left, right) => right.Version.CompareSortOrderTo(left.Version)); + + // Non-semver keys (such as four-part versions) trail the sorted entries rather than being dropped. + var ordered = new List(); + ordered.AddRange(parsed.ConvertAll(p => p.Entry)); + ordered.AddRange(unparsed); + + return ordered; + } + + // Only a leading "v" - a blanket Replace would corrupt versions such as "1.0.0-preview". + private static string StripVersionPrefix(string version) + { + return version.StartsWith("v", StringComparison.OrdinalIgnoreCase) ? version.Substring(1) : version; + } + } +} diff --git a/Stardrop/ViewModels/MainWindowViewModel.cs b/Stardrop/ViewModels/MainWindowViewModel.cs index 5ce97e0..219a5d8 100644 --- a/Stardrop/ViewModels/MainWindowViewModel.cs +++ b/Stardrop/ViewModels/MainWindowViewModel.cs @@ -59,6 +59,8 @@ public class MainWindowViewModel : ViewModelBase public bool ShowEndorsements { get { return _showEndorsements; } set { this.RaiseAndSetIfChanged(ref _showEndorsements, value); } } private bool _showInstalls; public bool ShowInstalls { get { return _showInstalls; } set { this.RaiseAndSetIfChanged(ref _showInstalls, value); } } + private bool _showChangelogs; + public bool ShowChangelogs { get { return _showChangelogs; } set { this.RaiseAndSetIfChanged(ref _showChangelogs, value); } } private string _filterText; public string FilterText { get { return _filterText; } set { _filterText = value; UpdateFilter(); } } private List _columnFilter; diff --git a/Stardrop/Views/ChangelogWindow.axaml b/Stardrop/Views/ChangelogWindow.axaml new file mode 100644 index 0000000..a6cdc6b --- /dev/null +++ b/Stardrop/Views/ChangelogWindow.axaml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +