diff --git a/Directory.Packages.props b/Directory.Packages.props
index 301024cf8a..d06b2d6cf8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,6 +17,7 @@
+
diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json
index b3cab7f5f6..e82d379a0f 100644
--- a/src/Ryujinx/Assets/Locales/en_US.json
+++ b/src/Ryujinx/Assets/Locales/en_US.json
@@ -775,8 +775,10 @@
"NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.",
"NetworkInterfaceDefault": "Default",
"PackagingShaders": "Packaging Shaders",
- "AboutChangelogButton": "View Changelog on GitHub",
- "AboutChangelogButtonTooltipMessage": "Click to open the changelog for this version in your default browser.",
+ "AboutChangelogButton": "View Changelog",
+ "AboutChangelogButtonTooltipMessage": "Click to view the changelog.",
+ "ChangelogWindowTitle": "Changelog",
+ "ChangelogDescription": "Showing the 10 most recent versions. For more changelog version history please visit Ryujinx GitHub page.",
"SettingsTabNetworkMultiplayer": "Multiplayer",
"MultiplayerMode": "Mode:",
"MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index 6718b7fcc4..989b281a89 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -44,6 +44,7 @@
+
diff --git a/src/Ryujinx/UI/Windows/AboutWindow.axaml b/src/Ryujinx/UI/Windows/AboutWindow.axaml
index 69fa82517a..cadd875bcf 100644
--- a/src/Ryujinx/UI/Windows/AboutWindow.axaml
+++ b/src/Ryujinx/UI/Windows/AboutWindow.axaml
@@ -87,8 +87,7 @@
Padding="5"
HorizontalAlignment="Center"
Background="Transparent"
- Click="Button_OnClick"
- Tag="https://github.com/Ryujinx/Ryujinx/wiki/Changelog#ryujinx-changelog">
+ Click="OpenChangelogWindow">
diff --git a/src/Ryujinx/UI/Windows/AboutWindow.axaml.cs b/src/Ryujinx/UI/Windows/AboutWindow.axaml.cs
index c32661b0ce..9663eec919 100644
--- a/src/Ryujinx/UI/Windows/AboutWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/AboutWindow.axaml.cs
@@ -3,6 +3,7 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Styling;
+using Avalonia.VisualTree;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
@@ -52,6 +53,18 @@ namespace Ryujinx.Ava.UI.Windows
}
}
+ private void OpenChangelogWindow(object sender, RoutedEventArgs e)
+ {
+ ChangelogWindow changelogWindow = new ChangelogWindow();
+
+ // Find the parent window to use as the owner for the modal dialog
+ var parentWindow = this.FindAncestorOfType();
+ if (parentWindow != null)
+ {
+ changelogWindow.ShowDialog(parentWindow); // Pass the parent window as the owner
+ }
+ }
+
private void AmiiboLabel_OnPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender is TextBlock)
diff --git a/src/Ryujinx/UI/Windows/ChangelogWindow.axaml b/src/Ryujinx/UI/Windows/ChangelogWindow.axaml
new file mode 100644
index 0000000000..f8f6821e8c
--- /dev/null
+++ b/src/Ryujinx/UI/Windows/ChangelogWindow.axaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/Windows/ChangelogWindow.axaml.cs b/src/Ryujinx/UI/Windows/ChangelogWindow.axaml.cs
new file mode 100644
index 0000000000..1992e59492
--- /dev/null
+++ b/src/Ryujinx/UI/Windows/ChangelogWindow.axaml.cs
@@ -0,0 +1,153 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Styling;
+using FluentAvalonia.UI.Controls;
+using HtmlAgilityPack;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.UI.Common.Helper;
+using System;
+using System.Diagnostics;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Button = Avalonia.Controls.Button;
+
+
+namespace Ryujinx.Ava.UI.Windows
+{
+ public partial class ChangelogWindow : StyleableWindow
+ {
+ public ChangelogWindow()
+ {
+ DataContext = this;
+ InitializeComponent();
+ InitializeAsync();
+ Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance[LocaleKeys.ChangelogWindowTitle];
+ }
+
+ private async void InitializeAsync()
+ {
+ try
+ {
+ LoadingTextBlock.IsVisible = true; // Show the loading text
+ ChangelogTextBlock.IsVisible = false; // Hide the changelog text initially
+
+ string changelogHtml = await FetchChangelogHtml();
+ string changelog = ParseChangelogForRecentVersions(changelogHtml, 10);
+ LoadChangelog(changelog);
+
+ LoadingTextBlock.IsVisible = false; // Hide the loading text
+ ChangelogTextBlock.IsVisible = true; // Show the changelog text
+ }
+ catch (Exception ex)
+ {
+ LoadingTextBlock.Text = "Failed to load changelog: " + ex.Message;
+ }
+ }
+
+ private void LoadChangelog(string changelog)
+ {
+ ChangelogTextBlock.Text = changelog;
+ }
+
+ private static async Task FetchChangelogHtml()
+ {
+ using var client = new HttpClient();
+ client.DefaultRequestHeaders.UserAgent.ParseAdd("Ryujinx-Updater/1.0.0");
+ return await client.GetStringAsync("https://github.com/Ryujinx/Ryujinx/wiki/Changelog");
+ }
+
+ private static string ParseChangelogForRecentVersions(string html, int count)
+ {
+ var doc = new HtmlDocument();
+ doc.LoadHtml(html);
+
+ var headers = doc.DocumentNode.SelectNodes("//div[contains(@class, 'markdown-heading')]//h2");
+ if (headers != null)
+ {
+ var content = new StringBuilder();
+ int versionsFound = 0;
+
+ foreach (var header in headers)
+ {
+ if (versionsFound >= count)
+ break; // Stop after finding the desired number of versions
+
+ content.Append(header.OuterHtml);
+ var currentNode = header.ParentNode.NextSibling;
+
+ while (currentNode != null && versionsFound < count)
+ {
+ if (currentNode.Name == "div" && currentNode.SelectSingleNode("h2") != null)
+ {
+ versionsFound++; // Increment for each version header found
+ if (versionsFound >= count)
+ break;
+ }
+ content.Append(currentNode.OuterHtml);
+ currentNode = currentNode.NextSibling;
+ }
+ }
+
+ return ConvertHtmlToPlainText(content.ToString());
+ }
+ return "No changelog found.";
+ }
+
+ private static string ConvertHtmlToPlainText(string html)
+ {
+ var doc = new HtmlDocument();
+ doc.LoadHtml(html);
+
+ // Recursively format nodes
+ string formattedText = FormatNode(doc.DocumentNode);
+
+ return HtmlEntity.DeEntitize(formattedText);
+ }
+
+ private static string FormatNode(HtmlNode node, int depth = 0)
+ {
+ StringBuilder sb = new StringBuilder();
+ foreach (var child in node.ChildNodes)
+ {
+ switch (child.Name)
+ {
+ case "ul":
+ // Recursively format the list items
+ sb.Append(FormatNode(child, depth));
+ sb.AppendLine();
+ break;
+
+ case "li":
+ // Format list item based on depth: "-" for top-level, "+" for nested items
+ string prefix = (depth == 0 ? "- " : new string(' ', depth * 4) + "+ ");
+ sb.AppendLine($"{prefix}{FormatNode(child, depth + 1).Trim()}");
+ break;
+
+ case "p":
+ case "#text": // Handling direct text nodes
+ if (!string.IsNullOrWhiteSpace(child.InnerText))
+ {
+ // Trim the text
+ string text = HtmlEntity.DeEntitize(child.InnerText).Trim();
+ sb.Append($"{text}\n");
+ }
+ break;
+
+ default:
+ // Recursively process other types of nodes
+ if (child.HasChildNodes)
+ {
+ sb.Append(FormatNode(child, depth)); // Keep current depth for other types
+ }
+ break;
+ }
+ }
+ return sb.ToString();
+ }
+ }
+}