From 92bcd2ef6286fda3f31cb2e3819b387be573cdbf Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:58:29 -0600 Subject: [PATCH] Working mod thumbnails via Nexus Mods --- Stardrop/Models/Mod.cs | 9 ++- Stardrop/Models/Nexus/Web/ModDetails.cs | 3 + Stardrop/Models/Settings.cs | 1 + Stardrop/Program.cs | 3 + Stardrop/Utilities/External/NexusClient.cs | 62 ++++++++++++++++++- Stardrop/Utilities/Pathing.cs | 5 ++ Stardrop/ViewModels/MainWindowViewModel.cs | 31 ++++++++++ .../ViewModels/SettingsWindowViewModel.cs | 3 + Stardrop/Views/MainWindow.axaml | 7 +++ Stardrop/Views/MainWindow.axaml.cs | 7 +++ Stardrop/Views/SettingsWindow.axaml | 1 + Stardrop/i18n/default.json | 2 + 12 files changed, 132 insertions(+), 2 deletions(-) diff --git a/Stardrop/Models/Mod.cs b/Stardrop/Models/Mod.cs index cdfec952..7d26e977 100644 --- a/Stardrop/Models/Mod.cs +++ b/Stardrop/Models/Mod.cs @@ -1,6 +1,10 @@ -using Semver; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Shared.PlatformSupport; +using Semver; using Stardrop.Models.Data.Enums; using Stardrop.Models.SMAPI; +using Stardrop.Utilities; using System; using System.Collections.Generic; using System.ComponentModel; @@ -42,6 +46,9 @@ public class Mod : INotifyPropertyChanged private string _modPageUri { get; set; } public string ModPageUri { get { return _modPageUri; } set { _modPageUri = value; NotifyPropertyChanged("ModPageUri"); } } public int? NexusModId { get { return GetNexusId(); } } + private string? _nexusModThumbnailPath { get; set; } + public string? NexusModThumbnailPath { get { return _nexusModThumbnailPath; } set { _nexusModThumbnailPath = value; NexusModThumbnailFile = string.IsNullOrEmpty(value) ? null : new Bitmap(value); NotifyPropertyChanged("NexusModThumbnailFile"); } } + public Bitmap? NexusModThumbnailFile { get; set; } private bool _isEnabled { get; set; } public bool IsEnabled { diff --git a/Stardrop/Models/Nexus/Web/ModDetails.cs b/Stardrop/Models/Nexus/Web/ModDetails.cs index ddaf5be9..f8cfd424 100644 --- a/Stardrop/Models/Nexus/Web/ModDetails.cs +++ b/Stardrop/Models/Nexus/Web/ModDetails.cs @@ -6,5 +6,8 @@ public class ModDetails { [JsonPropertyName("name")] public string? Name { get; set; } + + [JsonPropertyName("picture_url")] + public string? ThumbnailUrl { get; set; } } } diff --git a/Stardrop/Models/Settings.cs b/Stardrop/Models/Settings.cs index 50662814..a2ea2971 100644 --- a/Stardrop/Models/Settings.cs +++ b/Stardrop/Models/Settings.cs @@ -23,6 +23,7 @@ public class Settings /// public bool AlwaysAskToDelete { get; set; } = true; public bool ShouldAutomaticallySaveProfileChanges { get; set; } = true; + public bool ShowModThumbnails { get; set; } public NexusServers PreferredNexusServer { get; set; } = NexusServers.NexusCDN; public bool IsAskingBeforeAcceptingNXM { get; set; } = true; public GameDetails GameDetails { get; set; } diff --git a/Stardrop/Program.cs b/Stardrop/Program.cs index d1f6e1cd..2b7e97a0 100644 --- a/Stardrop/Program.cs +++ b/Stardrop/Program.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.ReactiveUI; +using Avalonia.Shared.PlatformSupport; using CommandLine; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.MaterialDesign; @@ -27,6 +28,7 @@ class Program internal static Helper helper; internal static Settings settings = new Settings(); internal static Translation translation = new Translation(); + internal static AssetLoader assetLoader = new AssetLoader(); internal static bool onBootStartSMAPI = false; internal static string? nxmLink = null; @@ -88,6 +90,7 @@ public static void Main(string[] args) Directory.CreateDirectory(Pathing.GetProfilesFolderPath()); Directory.CreateDirectory(Pathing.GetSelectedModsFolderPath()); Directory.CreateDirectory(Pathing.GetNexusPath()); + Directory.CreateDirectory(Pathing.GetThumbnailsPath()); Directory.CreateDirectory(Pathing.GetSmapiUpgradeFolderPath()); // Verify the settings folder path is created diff --git a/Stardrop/Utilities/External/NexusClient.cs b/Stardrop/Utilities/External/NexusClient.cs index 2317eb9f..1fe7cf34 100644 --- a/Stardrop/Utilities/External/NexusClient.cs +++ b/Stardrop/Utilities/External/NexusClient.cs @@ -480,7 +480,6 @@ public async Task> GetEndorsements() return new List(); } - public async Task SetModEndorsement(int modId, bool isEndorsed) { try @@ -542,6 +541,67 @@ public async Task SetModEndorsement(int modId, bool isEndor return EndorsementResponse.Unknown; } + public async Task DownloadThumbnail(int modId) + { + try + { + var response = await _client.GetAsync($"games/stardewvalley/mods/{modId}.json"); + if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null) + { + string content = await response.Content.ReadAsStringAsync(); + ModDetails modDetails = JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (modDetails is null) + { + Program.helper.Log($"Unable to get mod thumbnail for the mod {modId} on Nexus Mods"); + Program.helper.Log($"Response from Nexus Mods:\n{content}"); + + return null; + } + + UpdateRequestCounts(response.Headers); + + if (string.IsNullOrEmpty(modDetails.ThumbnailUrl)) + { + Program.helper.Log($"The mod {modId} does not have a valid thumbnail image available on Nexus Mods"); + Program.helper.Log($"Response from Nexus Mods:\n{content}"); + return null; + } + + // Download the thumbnail + var thumbnailPath = Path.Combine(Pathing.GetThumbnailsPath(), $"{modId}{Path.GetExtension(modDetails.ThumbnailUrl)}"); + using var fileStream = new FileStream(thumbnailPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 8192, useAsync: true); + using var downloadStream = await _client.GetStreamAsync(modDetails.ThumbnailUrl); + + await downloadStream.CopyToAsync(fileStream); + await fileStream.FlushAsync(); + + return thumbnailPath; + } + 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 mod thumbnail for the mod {modId} on Nexus Mods: {ex}", Helper.Status.Alert); + } + + return null; + } + private void UpdateRequestCounts(HttpResponseHeaders headers) { if (headers.TryGetValues("x-rl-daily-limit", out var limitValues) && Int32.TryParse(limitValues.First(), out int dailyLimit)) diff --git a/Stardrop/Utilities/Pathing.cs b/Stardrop/Utilities/Pathing.cs index 0b6daed3..2256bf23 100644 --- a/Stardrop/Utilities/Pathing.cs +++ b/Stardrop/Utilities/Pathing.cs @@ -106,6 +106,11 @@ public static string GetNexusPath() return Path.Combine(defaultHomePath, "Nexus"); } + public static string GetThumbnailsPath() + { + return Path.Combine(defaultHomePath, "Thumbnails", "Nexus"); + } + public static string GetSmapiUpgradeFolderPath() { return Path.Combine(defaultHomePath, "SMAPI"); diff --git a/Stardrop/ViewModels/MainWindowViewModel.cs b/Stardrop/ViewModels/MainWindowViewModel.cs index f7bfa86a..7a85b83a 100644 --- a/Stardrop/ViewModels/MainWindowViewModel.cs +++ b/Stardrop/ViewModels/MainWindowViewModel.cs @@ -75,6 +75,8 @@ public class MainWindowViewModel : ViewModelBase public bool ShowSaveProfileChanges { get { return _showSaveProfileChanges; } set { this.RaiseAndSetIfChanged(ref _showSaveProfileChanges, value); } } private bool _showSaveProfileChanges; public bool AreModGroupsEnabled { get { return Program.settings.ModGroupingMethod != ModGrouping.None; } } + public bool ShowModThumbnails { get { return _showModThumbnails; } set { this.RaiseAndSetIfChanged(ref _showModThumbnails, value); } } + private bool _showModThumbnails = Program.settings.ShowModThumbnails; public MainWindowViewModel(string modsFilePath, string version) { @@ -349,6 +351,11 @@ public void DiscoverMods(string modsFilePath) } } + if (Program.settings.ShowModThumbnails) + { + UpdateThumbnails(); + } + // Update the local data var modInstallData = new List(); foreach (var mod in Mods.Where(m => m is not null)) @@ -563,6 +570,30 @@ internal async void UpdateEndorsements() } } + internal async void UpdateThumbnails() + { + // Get all existing thumbnails + IEnumerable nexusModThumbnails = new List(); + var thumbnailDirectory = new DirectoryInfo(Pathing.GetThumbnailsPath()); + if (thumbnailDirectory.Exists) + { + nexusModThumbnails = thumbnailDirectory.EnumerateFiles(); + } + + foreach (var mod in Mods.Where(m => m.NexusModId is not null)) + { + var thumbnail = nexusModThumbnails.FirstOrDefault(t => mod.NexusModId is not null && Path.GetFileNameWithoutExtension(t.Name).Equals(mod.NexusModId.ToString(), StringComparison.OrdinalIgnoreCase)); + if (thumbnail is not null) + { + mod.NexusModThumbnailPath = thumbnail.FullName; + } + else if (Nexus.Client is not null && mod.NexusModThumbnailPath is null) + { + mod.NexusModThumbnailPath = await Nexus.Client.DownloadThumbnail((int)mod.NexusModId); + } + } + } + internal void ReadModConfigs(Profile profile) { ReadModConfigs(profile, GetPendingConfigUpdates(profile)); diff --git a/Stardrop/ViewModels/SettingsWindowViewModel.cs b/Stardrop/ViewModels/SettingsWindowViewModel.cs index 356998c5..f5beac46 100644 --- a/Stardrop/ViewModels/SettingsWindowViewModel.cs +++ b/Stardrop/ViewModels/SettingsWindowViewModel.cs @@ -16,6 +16,7 @@ public class SettingsWindowViewModel : ViewModelBase public bool EnableModsOnAdd { get { return Program.settings.EnableModsOnAdd; } set { Program.settings.EnableModsOnAdd = value; } } public bool AlwaysAskToDelete { get { return Program.settings.AlwaysAskToDelete; } set { Program.settings.AlwaysAskToDelete = value; } } public bool ShouldAutomaticallySaveProfileChanges { get { return Program.settings.ShouldAutomaticallySaveProfileChanges; } set { Program.settings.ShouldAutomaticallySaveProfileChanges = value; } } + public bool ShowModThumbnails { get { return Program.settings.ShowModThumbnails; } set { Program.settings.ShowModThumbnails = value; } } // Tooltips public string ToolTip_SMAPI { get; set; } @@ -31,6 +32,7 @@ public class SettingsWindowViewModel : ViewModelBase public string ToolTip_EnableProfileSpecificModConfigs { get; set; } public string ToolTip_EnableModsOnAdd { get; set; } public string ToolTip_ShouldAutomaticallySaveProfileChanges { get; set; } + public string ToolTip_ShowModThumbnails { get; set; } public string ToolTip_Save { get; set; } public string ToolTip_Cancel { get; set; } @@ -54,6 +56,7 @@ public SettingsWindowViewModel() ToolTip_EnableProfileSpecificModConfigs = Program.translation.Get("ui.settings_window.tooltips.enable_profile_specific_configs"); ToolTip_EnableModsOnAdd = Program.translation.Get("ui.settings_window.tooltips.enable_mods_on_add"); ToolTip_ShouldAutomaticallySaveProfileChanges = Program.translation.Get("ui.settings_window.tooltips.automatically_save_profile_changes"); + ToolTip_ShowModThumbnails = Program.translation.Get("ui.settings_window.tooltips.show_mod_thumbnails"); ToolTip_Save = Program.translation.Get("ui.settings_window.tooltips.save_changes"); ToolTip_Cancel = Program.translation.Get("ui.settings_window.tooltips.cancel_changes"); diff --git a/Stardrop/Views/MainWindow.axaml b/Stardrop/Views/MainWindow.axaml index 729c9bca..2fc653d7 100644 --- a/Stardrop/Views/MainWindow.axaml +++ b/Stardrop/Views/MainWindow.axaml @@ -502,6 +502,13 @@ + + + + + + + diff --git a/Stardrop/Views/MainWindow.axaml.cs b/Stardrop/Views/MainWindow.axaml.cs index e0d4cdd9..bbfe69ef 100644 --- a/Stardrop/Views/MainWindow.axaml.cs +++ b/Stardrop/Views/MainWindow.axaml.cs @@ -1247,6 +1247,7 @@ private async Task DisplaySettingsWindow() await HandleModListRefresh(); _viewModel.ShowSaveProfileChanges = !Program.settings.ShouldAutomaticallySaveProfileChanges; + _viewModel.ShowModThumbnails = Program.settings.ShowModThumbnails; } } @@ -2012,6 +2013,12 @@ private async Task CheckForNexusConnection() // Show endorsements _viewModel.ShowEndorsements = true; + // Show thumbnails + if (Program.settings.ShowModThumbnails) + { + _viewModel.UpdateThumbnails(); + } + // Show Nexus mod download column, if user is premium _viewModel.ShowInstalls = Program.settings.NexusDetails.IsPremium; } diff --git a/Stardrop/Views/SettingsWindow.axaml b/Stardrop/Views/SettingsWindow.axaml index 420ac1a8..4eff6575 100644 --- a/Stardrop/Views/SettingsWindow.axaml +++ b/Stardrop/Views/SettingsWindow.axaml @@ -218,6 +218,7 @@ + diff --git a/Stardrop/i18n/default.json b/Stardrop/i18n/default.json index ab0fb700..fb572306 100644 --- a/Stardrop/i18n/default.json +++ b/Stardrop/i18n/default.json @@ -115,6 +115,7 @@ "ui.settings_window.buttons.enable_mods_on_add": "Automatically Enable Mods On Install", "ui.settings_window.buttons.always_ask_to_delete": "Always Ask To Delete Mod Files On Update", "ui.settings_window.buttons.automatically_save_profile_changes": "Automatically Save Profile Changes", + "ui.settings_window.buttons.show_mod_thumbnails": "Show Mod Thumbnails", "ui.settings_window.buttons.always_ask_for_NXM_installs": "Always Ask Before Installing NXM Files", "ui.settings_window.buttons.register_nxm_association": "Register NXM Association", @@ -132,6 +133,7 @@ "ui.settings_window.tooltips.enable_mods_on_add": "If checked, Stardrop will automatically enable newly added or updated mods", "ui.settings_window.tooltips.always_ask_to_delete": "If checked, Stardrop will always ask whether to delete mod files when updating the mod", "ui.settings_window.tooltips.automatically_save_profile_changes": "If checked, Stardrop will automatically save changes made to the profile", + "ui.settings_window.tooltips.show_mod_thumbnails": "If checked, displays the mod's thumbnail from Nexus Mods (where applicable).\n\nRequires a Stardrop API key from Nexus Mods.'", "ui.settings_window.tooltips.preferred_server": "Sets your preferred server to use when downloading from Nexus Mods", "ui.settings_window.tooltips.save_changes": "Save Changes", "ui.settings_window.tooltips.cancel_changes": "Cancel",