Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Stardrop/Models/Data/Enums/ChangelogState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Stardrop.Models.Data.Enums
{
public enum ChangelogState
{
Unknown,
Fetching
}
}
13 changes: 12 additions & 1 deletion Stardrop/Models/Mod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"); } }

Expand Down
6 changes: 6 additions & 0 deletions Stardrop/Models/Nexus/Web/ChangelogVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using System.Collections.Generic;

namespace Stardrop.Models.Nexus.Web
{
public record ChangelogVersion(string Version, List<string> Changes);
}
50 changes: 50 additions & 0 deletions Stardrop/Utilities/External/NexusClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,56 @@ public async Task<EndorsementResponse> SetModEndorsement(int modId, bool isEndor
return EndorsementResponse.Unknown;
}

/// <summary>
/// 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.
/// </summary>
public async Task<Dictionary<string, List<string>>?> 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<Dictionary<string, List<string>>>(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<string?> DownloadThumbnail(int modId)
{
try
Expand Down
81 changes: 81 additions & 0 deletions Stardrop/ViewModels/ChangelogWindowViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<ChangelogVersion> 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<ChangelogVersion>();
}

/// <summary>
/// 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.
/// </summary>
public static List<ChangelogVersion> SortNewestFirst(Dictionary<string, List<string>> changelogs)
{
var parsed = new List<(SemVersion Version, ChangelogVersion Entry)>();
var unparsed = new List<ChangelogVersion>();

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<ChangelogVersion>();
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;
}
}
}
2 changes: 2 additions & 0 deletions Stardrop/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> _columnFilter;
Expand Down
165 changes: 165 additions & 0 deletions Stardrop/Views/ChangelogWindow.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<Window
x:Class="Stardrop.Views.ChangelogWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:i18n="clr-namespace:Stardrop.Utilities.Extension"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Stardrop.ViewModels"
Title="{i18n:Translate ui.window.changelog.name}"
Width="600"
Height="500"
MinWidth="500"
MinHeight="400"
d:DesignHeight="500"
d:DesignWidth="600"
Background="{DynamicResource ThemeBackgroundBrush}"
CanResize="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
ExtendClientAreaToDecorationsHint="true"
HasSystemDecorations="true"
Icon="/Assets/icon.ico"
mc:Ignorable="d">

<Window.DataContext>
<vm:ChangelogWindowViewModel />
</Window.DataContext>

<Window.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="LightGray" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="White" />
</Style>
<Style Selector="TextBlock.row">
<Setter Property="Foreground" Value="{DynamicResource DataGridRowForeground}" />
</Style>
<Style Selector="TextBlock.version">
<Setter Property="Foreground" Value="{DynamicResource HighlightForegroundBrush}" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="FontSize" Value="15" />
</Style>
</Window.Styles>

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>

<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<Border
Grid.Row="0"
Grid.ColumnSpan="2"
BorderBrush="{DynamicResource HighlightBrush}"
BorderThickness="0,0,0,2">
<Menu Name="menuBar" KeyboardNavigation.TabNavigation="None">
<Image Source="/Assets/icon.ico" Stretch="None" />
<TextBlock
Margin="-10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{DynamicResource ThemeForegroundBrush}"
Text="{Binding ModName}" />
</Menu>
</Border>
<ScrollViewer
Grid.Row="1"
Grid.ColumnSpan="2"
AllowAutoHide="True">
<StackPanel Margin="10,10,10,0">
<ProgressBar
Height="20"
Margin="0,20,0,0"
Foreground="{DynamicResource HighlightForegroundBrush}"
IsIndeterminate="True"
IsVisible="{Binding IsLoading}" />

<TextBlock
Margin="0,10,0,0"
Classes="row"
IsVisible="{Binding ShowEmptyMessage}"
Text="{Binding EmptyMessage}"
TextWrapping="Wrap" />

<ItemsControl IsVisible="{Binding HasChanges}" Items="{Binding Versions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,16">
<TextBlock
Margin="0,0,0,6"
Classes="version"
Text="{Binding Version}" />
<ItemsControl Items="{Binding Changes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="4,2,4,2" ColumnDefinitions="Auto *">
<TextBlock
Grid.Column="0"
Margin="0,0,6,0"
VerticalAlignment="Top"
Classes="row"
Text="&#8226;" />
<TextBlock
Grid.Column="1"
Classes="row"
Text="{Binding}"
TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>

<Border
Grid.Row="2"
Grid.ColumnSpan="2"
Height="4"
BorderBrush="{DynamicResource HighlightBrush}"
BorderThickness="0,0,0,2" />
<Grid
Grid.Row="3"
Grid.ColumnSpan="2"
Margin="0,15,0,15"
HorizontalAlignment="Right"
ColumnDefinitions="Auto, Auto"
RowDefinitions="Auto">
<Button
Name="modPageButton"
Grid.Column="0"
Margin="0,0,15,0"
HorizontalAlignment="Right"
Background="Transparent"
BorderBrush="{DynamicResource HighlightBrush}"
Content="{i18n:Translate ui.window.changelog.view_on_nexus}"
Foreground="{DynamicResource ThemeForegroundBrush}"
IsVisible="{Binding HasModPage}"
ToolTip.Tip="{i18n:Translate ui.window.changelog.view_on_nexus_tooltip}" />
<Button
Name="closeButton"
Grid.Column="1"
Margin="0,0,15,0"
HorizontalAlignment="Right"
i:Attached.Icon="mdi-check"
Background="Transparent"
BorderBrush="{DynamicResource HighlightBrush}"
Foreground="Green"
IsCancel="True"
ToolTip.Tip="{i18n:Translate internal.ok}" />
</Grid>
</Grid>
</Window>
Loading