From 53cc5eddecf35cca7c23394f04c4ceb188157ae5 Mon Sep 17 00:00:00 2001 From: Raymond Date: Mon, 11 Oct 2021 11:54:35 -0400 Subject: [PATCH] feat: PluginImageCache --- .../Interface/Internal/DalamudInterface.cs | 1 + .../Internal/Windows/PluginImageCache.cs | 488 ++++++++++++++++++ .../Internal/Windows/PluginInstallerWindow.cs | 352 +------------ 3 files changed, 509 insertions(+), 332 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/PluginImageCache.cs diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index c6b2b5119..097a31682 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -123,6 +123,7 @@ namespace Dalamud.Interface.Internal this.creditsWindow.Dispose(); this.consoleWindow.Dispose(); + this.pluginWindow.Dispose(); } #region Open diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs new file mode 100644 index 000000000..435d30e30 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -0,0 +1,488 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; +using ImGuiScene; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows +{ + /// + /// A cache for plugin icons and images. + /// + internal class PluginImageCache : IDisposable + { + /// + /// Maximum plugin image width. + /// + public const int PluginImageWidth = 730; + + /// + /// Maximum plugin image height. + /// + public const int PluginImageHeight = 380; + + /// + /// Maximum plugin icon width. + /// + public const int PluginIconWidth = 512; + + /// + /// Maximum plugin height. + /// + public const int PluginIconHeight = 512; + + // TODO: Change back to master after release + private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api4/{0}/{1}/images/{2}"; + + private readonly HttpClient httpClient = new(); + + private BlockingCollection> downloadQueue = new(); + private CancellationTokenSource downloadToken = new(); + + private Dictionary pluginIconMap = new(); + private Dictionary pluginImagesMap = new(); + + /// + /// Initializes a new instance of the class. + /// + public PluginImageCache() + { + var dalamud = Service.Get(); + var interfaceManager = Service.Get(); + + this.DefaultIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png")); + this.TroubleIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png")); + this.UpdateIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png")); + + var task = new Task( + () => this.DownloadTask(this.downloadToken.Token), + this.downloadToken.Token, + TaskCreationOptions.LongRunning); + task.Start(); + } + + /// + /// Gets the default plugin icon. + /// + public TextureWrap DefaultIcon { get; } + + /// + /// Gets the plugin trouble icon overlay. + /// + public TextureWrap TroubleIcon { get; } + + /// + /// Gets the plugin update icon overlay. + /// + public TextureWrap UpdateIcon { get; } + + /// + public void Dispose() + { + this.DefaultIcon?.Dispose(); + this.TroubleIcon?.Dispose(); + this.UpdateIcon?.Dispose(); + + this.downloadToken?.Cancel(); + this.downloadToken?.Dispose(); + this.downloadQueue?.CompleteAdding(); + this.downloadQueue?.Dispose(); + + foreach (var icon in this.pluginIconMap.Values) + { + icon?.Dispose(); + } + + foreach (var images in this.pluginImagesMap.Values) + { + foreach (var image in images) + { + image?.Dispose(); + } + } + + this.pluginIconMap.Clear(); + this.pluginImagesMap.Clear(); + } + + /// + /// Clear the cache of downloaded icons. + /// + public void ClearIconCache() + { + this.pluginIconMap.Clear(); + this.pluginImagesMap.Clear(); + } + + /// + /// Try to get the icon associated with the internal name of a plugin. + /// Uses the name within the manifest to search. + /// + /// The installed plugin, if available. + /// The plugin manifest. + /// If the plugin was third party sourced. + /// Cached image textures, or an empty array. + /// True if an entry exists, may be null if currently downloading. + public bool TryGetIcon(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, out TextureWrap? iconTexture) + { + if (this.pluginIconMap.TryGetValue(manifest.InternalName, out iconTexture)) + return true; + + iconTexture = null; + this.pluginIconMap.Add(manifest.InternalName, iconTexture); + + if (!this.downloadQueue.IsCompleted) + { + this.downloadQueue.Add(async () => await this.DownloadPluginIconAsync(plugin, manifest, isThirdParty)); + } + + return false; + } + + /// + /// Try to get any images associated with the internal name of a plugin. + /// Uses the name within the manifest to search. + /// + /// The installed plugin, if available. + /// The plugin manifest. + /// If the plugin was third party sourced. + /// Cached image textures, or an empty array. + /// True if the image array exists, may be empty if currently downloading. + public bool TryGetImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, out TextureWrap?[] imageTextures) + { + if (this.pluginImagesMap.TryGetValue(manifest.InternalName, out imageTextures)) + return true; + + imageTextures = Array.Empty(); + this.pluginImagesMap.Add(manifest.InternalName, imageTextures); + + if (!this.downloadQueue.IsCompleted) + { + this.downloadQueue.Add(async () => await this.DownloadPluginImagesAsync(plugin, manifest, isThirdParty)); + } + + return false; + } + + private async void DownloadTask(CancellationToken token) + { + while (true) + { + try + { + if (token.IsCancellationRequested) + return; + + if (!this.downloadQueue.TryTake(out var task, -1, token)) + return; + + await task.Invoke(); + } + catch (OperationCanceledException) + { + // Shutdown signal. + break; + } + catch (Exception ex) + { + Log.Error(ex, "An unhandled exception occurred in the plugin image downloader"); + } + } + + Log.Debug("Plugin image downloader has shutdown"); + } + + private async Task DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty) + { + var interfaceManager = Service.Get(); + var pluginManager = Service.Get(); + + static bool TryLoadIcon(byte[] bytes, string loc, PluginManifest manifest, InterfaceManager interfaceManager, out TextureWrap icon) + { + icon = interfaceManager.LoadImage(bytes); + + if (icon == null) + { + Log.Error($"Could not load icon for {manifest.InternalName} at {loc}"); + return false; + } + + if (icon.Width > PluginIconWidth || icon.Height > PluginIconHeight) + { + Log.Error($"Icon for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({PluginIconWidth}x{PluginIconHeight})."); + return false; + } + + if (icon.Height != icon.Width) + { + Log.Error($"Icon for {manifest.InternalName} at {loc} was not square."); + return false; + } + + return true; + } + + if (plugin != null && plugin.IsDev) + { + var file = this.GetPluginIconFileInfo(plugin); + if (file != null) + { + Log.Verbose($"Fetching icon for {manifest.InternalName} from {file.FullName}"); + + var bytes = await File.ReadAllBytesAsync(file.FullName); + if (!TryLoadIcon(bytes, file.FullName, manifest, interfaceManager, out var icon)) + return; + + this.pluginIconMap[manifest.InternalName] = icon; + Log.Verbose($"Plugin icon for {manifest.InternalName} loaded from disk"); + + return; + } + + // Dev plugins are likely going to look like a main repo plugin, the InstalledFrom field is going to be null. + // So instead, set the value manually so we download from the urls specified. + isThirdParty = true; + } + + var useTesting = pluginManager.UseTesting(manifest); + var url = this.GetPluginIconUrl(manifest, isThirdParty, useTesting); + + if (!url.IsNullOrEmpty()) + { + Log.Verbose($"Downloading icon for {manifest.InternalName} from {url}"); + + HttpResponseMessage data; + try + { + data = await this.httpClient.GetAsync(url); + } + catch (InvalidOperationException) + { + Log.Error($"Plugin icon for {manifest.InternalName} has an Invalid URI"); + return; + } + catch (Exception ex) + { + Log.Error(ex, $"An unexpected error occurred with the icon for {manifest.InternalName}"); + return; + } + + if (data.StatusCode == HttpStatusCode.NotFound) + return; + + data.EnsureSuccessStatusCode(); + + var bytes = await data.Content.ReadAsByteArrayAsync(); + if (!TryLoadIcon(bytes, url, manifest, interfaceManager, out var icon)) + return; + + this.pluginIconMap[manifest.InternalName] = icon; + Log.Verbose($"Plugin icon for {manifest.InternalName} downloaded"); + + return; + } + + Log.Verbose($"Plugin icon for {manifest.InternalName} is not available"); + } + + private async Task DownloadPluginImagesAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty) + { + var interfaceManager = Service.Get(); + var pluginManager = Service.Get(); + + static bool TryLoadImage(int i, byte[] bytes, string loc, PluginManifest manifest, InterfaceManager interfaceManager, out TextureWrap image) + { + image = interfaceManager.LoadImage(bytes); + + if (image == null) + { + Log.Error($"Could not load image{i + 1} for {manifest.InternalName} at {loc}"); + return false; + } + + if (image.Width > PluginImageWidth || image.Height > PluginImageHeight) + { + Log.Error($"Plugin image{i + 1} for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({PluginImageWidth}x{PluginImageHeight})."); + return false; + } + + return true; + } + + if (plugin != null && plugin.IsDev) + { + var files = this.GetPluginImageFileInfos(plugin); + if (files != null) + { + var didAny = false; + var pluginImages = new TextureWrap[files.Count]; + for (var i = 0; i < files.Count; i++) + { + var file = files[i]; + + if (file == null) + continue; + + Log.Verbose($"Loading image{i + 1} for {manifest.InternalName} from {file.FullName}"); + var bytes = await File.ReadAllBytesAsync(file.FullName); + + if (!TryLoadImage(i, bytes, file.FullName, manifest, interfaceManager, out var image)) + continue; + + Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} loaded from disk"); + pluginImages[i] = image; + + didAny = true; + } + + if (didAny) + { + Log.Verbose($"Plugin images for {manifest.InternalName} loaded from disk"); + + if (pluginImages.Contains(null)) + pluginImages = pluginImages.Where(image => image != null).ToArray(); + + this.pluginImagesMap[manifest.InternalName] = pluginImages; + + return; + } + } + + // Dev plugins are likely going to look like a main repo plugin, the InstalledFrom field is going to be null. + // So instead, set the value manually so we download from the urls specified. + isThirdParty = true; + } + + var useTesting = pluginManager.UseTesting(manifest); + var urls = this.GetPluginImageUrls(manifest, isThirdParty, useTesting); + if (urls != null) + { + var didAny = false; + var pluginImages = new TextureWrap[urls.Count]; + for (var i = 0; i < urls.Count; i++) + { + var url = urls[i]; + + if (url.IsNullOrEmpty()) + continue; + + Log.Verbose($"Downloading image{i + 1} for {manifest.InternalName} from {url}"); + + HttpResponseMessage data; + try + { + data = await this.httpClient.GetAsync(url); + } + catch (InvalidOperationException) + { + Log.Error($"Plugin image{i + 1} for {manifest.InternalName} has an Invalid URI"); + continue; + } + catch (Exception ex) + { + Log.Error(ex, $"An unexpected error occurred with image{i + 1} for {manifest.InternalName}"); + continue; + } + + if (data.StatusCode == HttpStatusCode.NotFound) + continue; + + data.EnsureSuccessStatusCode(); + + var bytes = await data.Content.ReadAsByteArrayAsync(); + if (!TryLoadImage(i, bytes, url, manifest, interfaceManager, out var image)) + continue; + + Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} downloaded"); + pluginImages[i] = image; + + didAny = true; + } + + if (didAny) + { + Log.Verbose($"Plugin images for {manifest.InternalName} downloaded"); + + if (pluginImages.Contains(null)) + pluginImages = pluginImages.Where(image => image != null).ToArray(); + + this.pluginImagesMap[manifest.InternalName] = pluginImages; + + return; + } + } + + Log.Verbose($"Images for {manifest.InternalName} are not available"); + } + + private string? GetPluginIconUrl(PluginManifest manifest, bool isThirdParty, bool isTesting) + { + if (isThirdParty) + return manifest.IconUrl; + + return MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, "icon.png"); + } + + private List? GetPluginImageUrls(PluginManifest manifest, bool isThirdParty, bool isTesting) + { + if (isThirdParty) + { + if (manifest.ImageUrls?.Count > 5) + { + Log.Warning($"Plugin {manifest.InternalName} has too many images"); + return manifest.ImageUrls.Take(5).ToList(); + } + + return manifest.ImageUrls; + } + + var output = new List(); + for (var i = 1; i <= 5; i++) + { + output.Add(MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, $"image{i}.png")); + } + + return output; + } + + private FileInfo? GetPluginIconFileInfo(LocalPlugin? plugin) + { + var pluginDir = plugin.DllFile.Directory; + + var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", "icon.png")); + if (devUrl.Exists) + return devUrl; + + return null; + } + + private List GetPluginImageFileInfos(LocalPlugin? plugin) + { + var pluginDir = plugin.DllFile.Directory; + var output = new List(); + for (var i = 1; i <= 5; i++) + { + var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", $"image{i}.png")); + if (devUrl.Exists) + { + output.Add(devUrl); + continue; + } + + output.Add(null); + } + + return output; + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs index 23d33ff7b..78161e138 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs @@ -5,8 +5,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Net; -using System.Net.Http; using System.Numerics; using System.Threading.Tasks; @@ -34,26 +32,13 @@ namespace Dalamud.Interface.Internal.Windows /// internal class PluginInstallerWindow : Window, IDisposable { - private const int PluginImageWidth = 730; - private const int PluginImageHeight = 380; - - private const int PluginIconWidth = 512; - private const int PluginIconHeight = 512; - - // TODO: Change back to master after release - private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api4/{0}/{1}/images/{2}"; - private static readonly ModuleLog Log = new("PLUGINW"); private readonly Vector4 changelogBgColor = new(0.114f, 0.584f, 0.192f, 0.678f); private readonly Vector4 changelogTextColor = new(0.812f, 1.000f, 0.816f, 1.000f); - private readonly TextureWrap defaultIcon; - private readonly TextureWrap troubleIcon; - private readonly TextureWrap updateIcon; - - private readonly HttpClient httpClient = new(); private readonly PluginCategoryManager categoryManager = new(); + private readonly PluginImageCache imageCache = new(); #region Image Tester State @@ -87,9 +72,6 @@ namespace Dalamud.Interface.Internal.Windows private List pluginListUpdatable = new(); private bool hasDevPlugins = false; - private Dictionary pluginIconMap = new(); - private Dictionary pluginImagesMap = new(); - private string searchText = string.Empty; private PluginSortKind sortKind = PluginSortKind.Alphabetical; @@ -119,8 +101,6 @@ namespace Dalamud.Interface.Internal.Windows MaximumSize = new Vector2(5000, 5000), }; - var dalamud = Service.Get(); - var interfaceManager = Service.Get(); var pluginManager = Service.Get(); // For debugging @@ -130,12 +110,6 @@ namespace Dalamud.Interface.Internal.Windows pluginManager.OnAvailablePluginsChanged += this.OnAvailablePluginsChanged; pluginManager.OnInstalledPluginsChanged += this.OnInstalledPluginsChanged; - this.defaultIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png")); - - this.troubleIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png")); - - this.updateIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png")); - for (var i = 0; i < this.testerImagePaths.Length; i++) { this.testerImagePaths[i] = string.Empty; @@ -165,9 +139,7 @@ namespace Dalamud.Interface.Internal.Windows pluginManager.OnAvailablePluginsChanged -= this.OnAvailablePluginsChanged; pluginManager.OnInstalledPluginsChanged -= this.OnInstalledPluginsChanged; - this.defaultIcon.Dispose(); - this.troubleIcon.Dispose(); - this.updateIcon.Dispose(); + this.imageCache?.Dispose(); } /// @@ -202,12 +174,11 @@ namespace Dalamud.Interface.Internal.Windows } /// - /// Clear the cache of downloaded icons. + /// Clear the icon and image caches, forcing a fresh download. /// public void ClearIconCache() { - this.pluginIconMap.Clear(); - this.pluginImagesMap.Clear(); + this.imageCache.ClearIconCache(); } private void DrawHeader() @@ -785,7 +756,7 @@ namespace Dalamud.Interface.Internal.Windows var hasIcon = this.testerIcon != null; - var iconTex = this.defaultIcon; + var iconTex = this.imageCache.DefaultIcon; if (hasIcon) iconTex = this.testerIcon; var iconSize = ImGuiHelpers.ScaledVector2(64, 64); @@ -797,13 +768,13 @@ namespace Dalamud.Interface.Internal.Windows if (this.testerError) { ImGui.SetCursorPos(cursorBeforeImage); - ImGui.Image(this.troubleIcon.ImGuiHandle, iconSize); + ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize); ImGui.SameLine(); } else if (this.testerUpdateAvailable) { ImGui.SetCursorPos(cursorBeforeImage); - ImGui.Image(this.updateIcon.ImGuiHandle, iconSize); + ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); ImGui.SameLine(); } @@ -869,7 +840,7 @@ namespace Dalamud.Interface.Internal.Windows if (ImGui.BeginChild( "pluginTestingImageScrolling", - new Vector2(width - (70 * ImGuiHelpers.GlobalScale), (PluginImageHeight / thumbFactor) + scrollBarSize), + new Vector2(width - (70 * ImGuiHelpers.GlobalScale), (PluginImageCache.PluginImageHeight / thumbFactor) + scrollBarSize), false, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoScrollWithMouse | @@ -902,8 +873,8 @@ namespace Dalamud.Interface.Internal.Windows float xAct = image.Width; float yAct = image.Height; - float xMax = PluginImageWidth; - float yMax = PluginImageHeight; + float xMax = PluginImageCache.PluginImageWidth; + float yMax = PluginImageCache.PluginImageHeight; // scale image if undersized if (xAct < xMax && yAct < yMax) @@ -1059,9 +1030,9 @@ namespace Dalamud.Interface.Internal.Windows ImGui.SetCursorPos(startCursor); - var iconTex = this.defaultIcon; - - if (this.pluginIconMap.TryGetValue(manifest.InternalName, out var cachedIconTex) && cachedIconTex != null) + var iconTex = this.imageCache.DefaultIcon; + var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex); + if (hasIcon && cachedIconTex != null) { iconTex = cachedIconTex; } @@ -1075,13 +1046,13 @@ namespace Dalamud.Interface.Internal.Windows if (updateAvailable) { ImGui.SetCursorPos(cursorBeforeImage); - ImGui.Image(this.updateIcon.ImGuiHandle, iconSize); + ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); ImGui.SameLine(); } else if (trouble) { ImGui.SetCursorPos(cursorBeforeImage); - ImGui.Image(this.troubleIcon.ImGuiHandle, iconSize); + ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize); ImGui.SameLine(); } @@ -1817,23 +1788,8 @@ namespace Dalamud.Interface.Internal.Windows private bool DrawPluginImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, int index) { - var hasImages = this.pluginImagesMap.TryGetValue(manifest.InternalName, out var imageTextures); - if (!hasImages) - { - this.pluginImagesMap.Add(manifest.InternalName, Array.Empty()); - Task.Run(async () => await this.DownloadPluginImagesAsync(plugin, manifest, isThirdParty)) - .ContinueWith(task => - { - if (task.IsFaulted) - { - Log.Error(task.Exception.InnerException, "An unhandled exception occurred in the plugin image downloader"); - } - }); - - return false; - } - - if (imageTextures.Length == 0) + var hasImages = this.imageCache.TryGetImages(plugin, manifest, isThirdParty, out var imageTextures); + if (!hasImages || imageTextures.Length == 0) return false; const float thumbFactor = 2.7f; @@ -1844,7 +1800,7 @@ namespace Dalamud.Interface.Internal.Windows var width = ImGui.GetWindowWidth(); - if (ImGui.BeginChild($"plugin{index}ImageScrolling", new Vector2(width - (70 * ImGuiHelpers.GlobalScale), (PluginImageHeight / thumbFactor) + scrollBarSize), false, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoBackground)) + if (ImGui.BeginChild($"plugin{index}ImageScrolling", new Vector2(width - (70 * ImGuiHelpers.GlobalScale), (PluginImageCache.PluginImageHeight / thumbFactor) + scrollBarSize), false, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoBackground)) { for (var i = 0; i < imageTextures.Length; i++) { @@ -1871,8 +1827,8 @@ namespace Dalamud.Interface.Internal.Windows float xAct = image.Width; float yAct = image.Height; - float xMax = PluginImageWidth; - float yMax = PluginImageHeight; + float xMax = PluginImageCache.PluginImageWidth; + float yMax = PluginImageCache.PluginImageHeight; // scale image if undersized if (xAct < xMax && yAct < yMax) @@ -1937,7 +1893,6 @@ namespace Dalamud.Interface.Internal.Windows this.ResortPlugins(); this.UpdateCategoriesOnPluginsChange(); - Task.Run(() => this.DownloadPluginIconsAsync(this.pluginListAvailable.ToArray())); } private void OnInstalledPluginsChanged() @@ -2029,273 +1984,6 @@ namespace Dalamud.Interface.Internal.Windows this.errorModalOnNextFrame = true; } - private async Task DownloadPluginIconsAsync(RemotePluginManifest[] plugins) - { - Log.Verbose("Starting icon download..."); - foreach (var plugin in plugins.Where(x => !this.pluginIconMap.ContainsKey(x.InternalName))) - { - this.pluginIconMap[plugin.InternalName] = null; - await this.DownloadPluginIconAsync(plugin, plugin.SourceRepo.IsThirdParty); - } - } - - private async Task DownloadPluginIconAsync(PluginManifest manifest, bool isThirdParty) - { - var interfaceManager = Service.Get(); - var pluginManager = Service.Get(); - - static bool TryLoadIcon(byte[] bytes, string loc, PluginManifest manifest, InterfaceManager interfaceManager, out TextureWrap icon) - { - icon = interfaceManager.LoadImage(bytes); - - if (icon == null) - { - Log.Error($"Could not load icon for {manifest.InternalName} at {loc}"); - return false; - } - - if (icon.Width > PluginIconWidth || icon.Height > PluginIconHeight) - { - Log.Error($"Icon for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({PluginIconWidth}x{PluginIconHeight})."); - return false; - } - - if (icon.Height != icon.Width) - { - Log.Error($"Icon for {manifest.InternalName} at {loc} was not square."); - return false; - } - - return true; - } - - var useTesting = pluginManager.UseTesting(manifest); - var url = this.GetPluginIconUrl(manifest, isThirdParty, useTesting); - if (url != null) - { - Log.Verbose($"Downloading icon for {manifest.InternalName} from {url}"); - - HttpResponseMessage data; - try - { - data = await this.httpClient.GetAsync(url); - } - catch (InvalidOperationException) - { - Log.Error($"Plugin icon for {manifest.InternalName} has an Invalid URI"); - return; - } - catch (Exception ex) - { - Log.Error(ex, $"An unexpected error occurred with the icon for {manifest.InternalName}"); - return; - } - - if (data.StatusCode == HttpStatusCode.NotFound) - return; - - data.EnsureSuccessStatusCode(); - - var bytes = await data.Content.ReadAsByteArrayAsync(); - if (!TryLoadIcon(bytes, url, manifest, interfaceManager, out var icon)) - return; - - this.pluginIconMap[manifest.InternalName] = icon; - Log.Verbose($"Plugin icon for {manifest.InternalName} downloaded"); - - return; - } - - Log.Verbose($"Plugin icon for {manifest.InternalName} is not available"); - } - - private async Task DownloadPluginImagesAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty) - { - var interfaceManager = Service.Get(); - var pluginManager = Service.Get(); - - static bool TryLoadImage(int i, byte[] bytes, string loc, PluginManifest manifest, InterfaceManager interfaceManager, out TextureWrap image) - { - image = interfaceManager.LoadImage(bytes); - - if (image == null) - { - Log.Error($"Could not load image{i + 1} for {manifest.InternalName} at {loc}"); - return false; - } - - if (image.Width > PluginImageWidth || image.Height > PluginImageHeight) - { - Log.Error($"Plugin image{i + 1} for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({PluginImageWidth}x{PluginImageHeight})."); - return false; - } - - return true; - } - - if (plugin != null && plugin.IsDev) - { - var files = this.GetPluginImageFileInfos(plugin); - if (files != null) - { - var didAny = false; - var pluginImages = new TextureWrap[files.Count]; - for (var i = 0; i < files.Count; i++) - { - var file = files[i]; - - if (file == null) - continue; - - Log.Verbose($"Loading image{i + 1} for {manifest.InternalName} from {file.FullName}"); - var bytes = await File.ReadAllBytesAsync(file.FullName); - - if (!TryLoadImage(i, bytes, file.FullName, manifest, interfaceManager, out var image)) - continue; - - Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} loaded from disk"); - pluginImages[i] = image; - - didAny = true; - } - - if (didAny) - { - Log.Verbose($"Plugin images for {manifest.InternalName} loaded from disk"); - - if (pluginImages.Contains(null)) - pluginImages = pluginImages.Where(image => image != null).ToArray(); - - this.pluginImagesMap[manifest.InternalName] = pluginImages; - - return; - } - } - - // Dev plugins are likely going to look like a main repo plugin, the InstalledFrom field is going to be null. - // So instead, set the value manually so we download from the urls specified. - isThirdParty = true; - } - - var useTesting = pluginManager.UseTesting(manifest); - var urls = this.GetPluginImageUrls(manifest, isThirdParty, useTesting); - if (urls != null) - { - var didAny = false; - var pluginImages = new TextureWrap[urls.Count]; - for (var i = 0; i < urls.Count; i++) - { - var url = urls[i]; - - Log.Verbose($"Downloading image{i + 1} for {manifest.InternalName} from {url}"); - - HttpResponseMessage data; - try - { - data = await this.httpClient.GetAsync(url); - } - catch (InvalidOperationException) - { - Log.Error($"Plugin image{i + 1} for {manifest.InternalName} has an Invalid URI"); - continue; - } - catch (Exception ex) - { - Log.Error(ex, $"An unexpected error occurred with image{i + 1} for {manifest.InternalName}"); - continue; - } - - if (data.StatusCode == HttpStatusCode.NotFound) - continue; - - data.EnsureSuccessStatusCode(); - - var bytes = await data.Content.ReadAsByteArrayAsync(); - if (!TryLoadImage(i, bytes, url, manifest, interfaceManager, out var image)) - continue; - - Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} downloaded"); - pluginImages[i] = image; - - didAny = true; - } - - if (didAny) - { - Log.Verbose($"Plugin images for {manifest.InternalName} downloaded"); - - if (pluginImages.Contains(null)) - pluginImages = pluginImages.Where(image => image != null).ToArray(); - - this.pluginImagesMap[manifest.InternalName] = pluginImages; - - return; - } - } - - Log.Verbose($"Images for {manifest.InternalName} are not available"); - } - - private string? GetPluginIconUrl(PluginManifest manifest, bool isThirdParty, bool isTesting) - { - if (isThirdParty) - return manifest.IconUrl; - - return MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, "icon.png"); - } - - private List? GetPluginImageUrls(PluginManifest manifest, bool isThirdParty, bool isTesting) - { - if (isThirdParty) - { - if (manifest.ImageUrls?.Count > 5) - { - Log.Warning($"Plugin {manifest.InternalName} has too many images"); - return manifest.ImageUrls.Take(5).ToList(); - } - - return manifest.ImageUrls; - } - - var output = new List(); - for (var i = 1; i <= 5; i++) - { - output.Add(MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, $"image{i}.png")); - } - - return output; - } - - private FileInfo? GetPluginIconFileInfo(LocalPlugin? plugin) - { - var pluginDir = plugin.DllFile.Directory; - - var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", "icon.png")); - if (devUrl.Exists) - return devUrl; - - return null; - } - - private List GetPluginImageFileInfos(LocalPlugin? plugin) - { - var pluginDir = plugin.DllFile.Directory; - var output = new List(); - for (var i = 1; i <= 5; i++) - { - var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", $"image{i}.png")); - if (devUrl.Exists) - { - output.Add(devUrl); - continue; - } - - output.Add(null); - } - - return output; - } - private void UpdateCategoriesOnSearchChange() { if (string.IsNullOrEmpty(this.searchText))