diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index c064920d34..e92eefdf52 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.12.0")] -[assembly: AssemblyInformationalVersion("8.12.0")] +[assembly: AssemblyFileVersion("8.12.1")] +[assembly: AssemblyInformationalVersion("8.12.1")] diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index 485fd7f965..9d09bf2f0d 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -25,6 +25,8 @@ namespace Umbraco.Core.IO public static string AppPlugins => "~/App_Plugins"; + public static string AppPluginIcons => "/Backoffice/Icons"; + public static string MvcViews => "~/Views"; public static string PartialViews => MvcViews + "/Partials/"; diff --git a/src/Umbraco.Core/Services/IIconService.cs b/src/Umbraco.Core/Services/IIconService.cs index 963edb22a5..1e1ae38002 100644 --- a/src/Umbraco.Core/Services/IIconService.cs +++ b/src/Umbraco.Core/Services/IIconService.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models; namespace Umbraco.Core.Services @@ -6,7 +8,7 @@ namespace Umbraco.Core.Services public interface IIconService { /// - /// Gets an IconModel containing the icon name and SvgString according to an icon name found at the global icons path + /// Gets the svg string for the icon name found at the global icons path /// /// /// @@ -15,7 +17,15 @@ namespace Umbraco.Core.Services /// /// Gets a list of all svg icons found at at the global icons path. /// - /// + /// A list of + [Obsolete("This method should not be used - use GetIcons instead")] + [EditorBrowsable(EditorBrowsableState.Never)] IList GetAllIcons(); + + /// + /// Gets a list of all svg icons found at at the global icons path. + /// + /// + IReadOnlyDictionary GetIcons(); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js index 87d976f6d9..73a9617aee 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js @@ -75,9 +75,8 @@ Icon with additional attribute. It can be treated like any other dom element iconHelper.getIcon(icon) .then(data => { - if (data !== null && data.svgString !== undefined) { + if (data && data.svgString) { // Watch source SVG string - //icon.svgString.$$unwrapTrustedValue(); scope.svgString = data.svgString; } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js index f26763bd14..f3f5deb695 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js @@ -3,9 +3,9 @@ * @name umbraco.services.iconHelper * @description A helper service for dealing with icons, mostly dealing with legacy tree icons **/ -function iconHelper($http, $q, $sce, $timeout, umbRequestHelper) { +function iconHelper($http, $q, $sce, $timeout) { - var converter = [ + const converter = [ { oldIcon: ".sprNew", newIcon: "add" }, { oldIcon: ".sprDelete", newIcon: "remove" }, { oldIcon: ".sprMove", newIcon: "enter" }, @@ -85,15 +85,61 @@ function iconHelper($http, $q, $sce, $timeout, umbRequestHelper) { { oldIcon: ".sprTreeDeveloperPython", newIcon: "icon-linux" } ]; - var collectedIcons; + let collectedIcons; - var imageConverter = [ - {oldImage: "contour.png", newIcon: "icon-umb-contour"} - ]; + let imageConverter = [ + {oldImage: "contour.png", newIcon: "icon-umb-contour"} + ]; - var iconCache = []; - var liveRequests = []; - var allIconsRequested = false; + const iconCache = []; + const promiseQueue = []; + let resourceLoadStatus = "none"; + + /** + * This is the same approach as use for loading the localized text json + * We don't want multiple requests for the icon collection, so need to track + * the current request state, and resolve the queued requests once the icons arrive + * Subsequent requests are returned immediately as the icons are cached into + */ + function init() { + const deferred = $q.defer(); + + if (resourceLoadStatus === "loaded") { + deferred.resolve(iconCache); + return deferred.promise; + } + + if (resourceLoadStatus === "loading") { + promiseQueue.push(deferred); + return deferred.promise; + } + + resourceLoadStatus = "loading"; + + $http({ method: "GET", url: Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl + 'GetIcons' }) + .then(function (response) { + resourceLoadStatus = "loaded"; + + for (const [key, value] of Object.entries(response.data.Data)) { + iconCache.push({name: key, svgString: $sce.trustAsHtml(value)}) + } + + deferred.resolve(iconCache); + + //ensure all other queued promises are resolved + for (let p in promiseQueue) { + promiseQueue[p].resolve(iconCache); + } + }, function (err) { + deferred.reject("Something broke"); + //ensure all other queued promises are resolved + for (let p in promiseQueue) { + promiseQueue[p].reject("Something broke"); + } + }); + + return deferred.promise; + } return { @@ -187,67 +233,12 @@ function iconHelper($http, $q, $sce, $timeout, umbRequestHelper) { /** Gets a single IconModel */ getIcon: function(iconName) { - return $q((resolve, reject) => { - var icon = this._getIconFromCache(iconName); - - if(icon !== undefined) { - resolve(icon); - } else { - var iconRequestPath = Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl + 'GetIcon?iconName=' + iconName; - - // If the current icon is being requested, wait a bit so that we don't have to make another http request and can instead get the icon from the cache. - // This is a bit rough and ready and could probably be improved used an event based system - if(liveRequests.indexOf(iconRequestPath) >= 0) { - setTimeout(() => { - resolve(this.getIcon(iconName)); - }, 10); - } else { - liveRequests.push(iconRequestPath); - // TODO - fix bug where Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl is undefinied when help icon - umbRequestHelper.resourcePromise( - $http.get(iconRequestPath) - ,'Failed to retrieve icon: ' + iconName) - .then(icon => { - if(icon) { - var trustedIcon = this.defineIcon(icon.Name, icon.SvgString); - - liveRequests = _.filter(liveRequests, iconRequestPath); - - resolve(trustedIcon); - } - }) - .catch(err => { - console.warn(err); - }); - }; - - } - }); + return init().then(icons => icons.find(i => i.name === iconName)); }, /** Gets all the available icons in the backoffice icon folder and returns them as an array of IconModels */ getAllIcons: function() { - return $q((resolve, reject) => { - if(allIconsRequested === false) { - allIconsRequested = true; - - umbRequestHelper.resourcePromise( - $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl + 'GetAllIcons') - ,'Failed to retrieve icons') - .then(icons => { - icons.forEach(icon => { - this.defineIcon(icon.Name, icon.SvgString); - }); - - resolve(iconCache); - }) - .catch(err => { - console.warn(err); - });; - } else { - resolve(iconCache); - } - }); + return init().then(icons => icons); }, /** LEGACY - Return a list of icons from icon fonts, optionally filter them */ @@ -312,9 +303,8 @@ function iconHelper($http, $q, $sce, $timeout, umbRequestHelper) { }, /** Returns the cached icon or undefined */ - _getIconFromCache: function(iconName) { - return _.find(iconCache, {name: iconName}); - } + _getIconFromCache: iconName => iconCache.find(icon => icon.name === iconName) + }; } angular.module('umbraco.services').factory('iconHelper', iconHelper); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 6da1e7bcdf..eb6649a7c4 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -347,9 +347,9 @@ False True - 8120 + 8121 / - http://localhost:8120 + http://localhost:8121 False False diff --git a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml index eefd182aff..5166815a1c 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml @@ -121,12 +121,6 @@ @Html.AngularValueResetPasswordCodeInfoScript(ViewData["PasswordResetCode"]) @Html.AngularValueTinyMceAssets() - app.run(["iconHelper", function (iconHelper) { - @* We inject icons to the icon helper(service), since icons can only be loaded if user is authorized. By injecting these to the service they will not be requested as they will become cached. *@ - iconHelper.defineIcon("icon-check", '@Html.Raw(Model.IconCheckData)'); - iconHelper.defineIcon("icon-delete", '@Html.Raw(Model.IconDeleteData)'); - }]); - //required for the noscript trick document.getElementById("mainwrapper").style.display = "inherit"; diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 92b67cbf1b..90e75479be 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -1,4 +1,8 @@ -using System; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin.Security; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -6,24 +10,19 @@ using System.Threading; using System.Threading.Tasks; using System.Web.Mvc; using System.Web.UI; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; -using Microsoft.Owin.Security; -using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.Models.Identity; -using Umbraco.Web.Models; -using Umbraco.Web.Mvc; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Features; using Umbraco.Web.JavaScript; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; using Umbraco.Web.Security; -using Umbraco.Web.Services; using Constants = Umbraco.Core.Constants; using JArray = Newtonsoft.Json.Linq.JArray; @@ -40,11 +39,9 @@ namespace Umbraco.Web.Editors private readonly ManifestParser _manifestParser; private readonly UmbracoFeatures _features; private readonly IRuntimeState _runtimeState; - private readonly IIconService _iconService; private BackOfficeUserManager _userManager; private BackOfficeSignInManager _signInManager; - [Obsolete("Use the constructor that injects IIconService.")] public BackOfficeController( ManifestParser manifestParser, UmbracoFeatures features, @@ -55,37 +52,11 @@ namespace Umbraco.Web.Editors IProfilingLogger profilingLogger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) - : this(manifestParser, - features, - globalSettings, - umbracoContextAccessor, - services, - appCaches, - profilingLogger, - runtimeState, - umbracoHelper, - Current.IconService) - { - - } - - public BackOfficeController( - ManifestParser manifestParser, - UmbracoFeatures features, - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IRuntimeState runtimeState, - UmbracoHelper umbracoHelper, - IIconService iconService) : base(globalSettings, umbracoContextAccessor, services, appCaches, profilingLogger, umbracoHelper) { _manifestParser = manifestParser; _features = features; _runtimeState = runtimeState; - _iconService = iconService; } protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager()); @@ -100,7 +71,7 @@ namespace Umbraco.Web.Editors /// public async Task Default() { - var backofficeModel = new BackOfficeModel(_features, GlobalSettings, _iconService); + var backofficeModel = new BackOfficeModel(_features, GlobalSettings); return await RenderDefaultOrProcessExternalLoginAsync( () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", backofficeModel), () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", backofficeModel)); @@ -186,7 +157,7 @@ namespace Umbraco.Web.Editors { return await RenderDefaultOrProcessExternalLoginAsync( //The default view to render when there is no external login info or errors - () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml", new BackOfficeModel(_features, GlobalSettings, _iconService)), + () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml", new BackOfficeModel(_features, GlobalSettings)), //The ActionResult to perform if external login is successful () => Redirect("/")); } diff --git a/src/Umbraco.Web/Editors/BackOfficeModel.cs b/src/Umbraco.Web/Editors/BackOfficeModel.cs index cbdafd2e94..d0d2e324f3 100644 --- a/src/Umbraco.Web/Editors/BackOfficeModel.cs +++ b/src/Umbraco.Web/Editors/BackOfficeModel.cs @@ -1,32 +1,19 @@ using System; using Umbraco.Core.Configuration; -using Umbraco.Core.Services; -using Umbraco.Web.Composing; using Umbraco.Web.Features; namespace Umbraco.Web.Editors { - public class BackOfficeModel { - - [Obsolete("Use the overload that injects IIconService.")] - public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings) : this(features, globalSettings, Current.IconService) - { - - } - public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings, IIconService iconService) + public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings) { Features = features; GlobalSettings = globalSettings; - IconCheckData = iconService.GetIcon("icon-check")?.SvgString; - IconDeleteData = iconService.GetIcon("icon-delete")?.SvgString; } public UmbracoFeatures Features { get; } public IGlobalSettings GlobalSettings { get; } - public string IconCheckData { get; } - public string IconDeleteData { get; } } } diff --git a/src/Umbraco.Web/Editors/BackOfficePreviewModel.cs b/src/Umbraco.Web/Editors/BackOfficePreviewModel.cs index cc7356b687..6ace8e7198 100644 --- a/src/Umbraco.Web/Editors/BackOfficePreviewModel.cs +++ b/src/Umbraco.Web/Editors/BackOfficePreviewModel.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Core.Configuration; using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Composing; using Umbraco.Web.Features; namespace Umbraco.Web.Editors @@ -13,21 +10,11 @@ namespace Umbraco.Web.Editors private readonly UmbracoFeatures _features; public IEnumerable Languages { get; } - [Obsolete("Use the overload that injects IIconService.")] public BackOfficePreviewModel( UmbracoFeatures features, IGlobalSettings globalSettings, IEnumerable languages) - : this(features, globalSettings, languages, Current.IconService) - { - } - - public BackOfficePreviewModel( - UmbracoFeatures features, - IGlobalSettings globalSettings, - IEnumerable languages, - IIconService iconService) - : base(features, globalSettings, iconService) + : base(features, globalSettings) { _features = features; Languages = languages; diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index dd4dd67681..6e2eeff3aa 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -1,4 +1,6 @@ -using System; +using ClientDependency.Core.Config; +using Microsoft.Owin; +using System; using System.Collections; using System.Collections.Generic; using System.Configuration; @@ -7,15 +9,11 @@ using System.Runtime.Serialization; using System.Web; using System.Web.Configuration; using System.Web.Mvc; -using ClientDependency.Core.Config; -using Microsoft.Owin; using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; -using Umbraco.Web.Controllers; using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.Models.ContentEditing; diff --git a/src/Umbraco.Web/Editors/IconController.cs b/src/Umbraco.Web/Editors/IconController.cs index 87303a4e62..2aac92088d 100644 --- a/src/Umbraco.Web/Editors/IconController.cs +++ b/src/Umbraco.Web/Editors/IconController.cs @@ -1,13 +1,20 @@ -using System.Collections.Generic; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { [PluginController("UmbracoApi")] - public class IconController : UmbracoAuthorizedApiController + [IsBackOffice] + [UmbracoWebApiRequireHttps] + [UnhandedExceptionLoggerConfiguration] + [EnableDetailedErrors] + public class IconController : UmbracoApiController { private readonly IIconService _iconService; @@ -30,9 +37,22 @@ namespace Umbraco.Web.Editors /// Gets a list of all svg icons found at at the global icons path. /// /// + [Obsolete("This method should not be used - use GetIcons instead")] public IList GetAllIcons() { return _iconService.GetAllIcons(); } + + /// + /// Gets a list of all svg icons found at at the global icons path. + /// + /// + public JsonNetResult GetIcons() + { + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { + Data = _iconService.GetIcons(), + Formatting = Formatting.None + }; + } } } diff --git a/src/Umbraco.Web/Editors/PreviewController.cs b/src/Umbraco.Web/Editors/PreviewController.cs index f00805d2dc..e2770b14ba 100644 --- a/src/Umbraco.Web/Editors/PreviewController.cs +++ b/src/Umbraco.Web/Editors/PreviewController.cs @@ -9,10 +9,8 @@ using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Features; using Umbraco.Web.JavaScript; -using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Services; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors @@ -25,39 +23,19 @@ namespace Umbraco.Web.Editors private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly ILocalizationService _localizationService; - private readonly IIconService _iconService; - [Obsolete("Use the constructor that injects IIconService.")] public PreviewController( UmbracoFeatures features, IGlobalSettings globalSettings, IPublishedSnapshotService publishedSnapshotService, IUmbracoContextAccessor umbracoContextAccessor, ILocalizationService localizationService) - :this(features, - globalSettings, - publishedSnapshotService, - umbracoContextAccessor, - localizationService, - Current.IconService) - { - - } - - public PreviewController( - UmbracoFeatures features, - IGlobalSettings globalSettings, - IPublishedSnapshotService publishedSnapshotService, - IUmbracoContextAccessor umbracoContextAccessor, - ILocalizationService localizationService, - IIconService iconService) { _features = features; _globalSettings = globalSettings; _publishedSnapshotService = publishedSnapshotService; _umbracoContextAccessor = umbracoContextAccessor; _localizationService = localizationService; - _iconService = iconService; } [UmbracoAuthorize(redirectToUmbracoLogin: true)] @@ -74,7 +52,7 @@ namespace Umbraco.Web.Editors availableLanguages = availableLanguages.Where(language => content.Cultures.ContainsKey(language.IsoCode)); } - var model = new BackOfficePreviewModel(_features, _globalSettings, availableLanguages, _iconService); + var model = new BackOfficePreviewModel(_features, _globalSettings, availableLanguages); if (model.PreviewExtendedHeaderView.IsNullOrWhiteSpace() == false) { diff --git a/src/Umbraco.Web/Services/IconService.cs b/src/Umbraco.Web/Services/IconService.cs index 175650fd12..15e673e6ba 100644 --- a/src/Umbraco.Web/Services/IconService.cs +++ b/src/Umbraco.Web/Services/IconService.cs @@ -1,8 +1,10 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; using Ganss.XSS; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -14,31 +16,43 @@ namespace Umbraco.Web.Services { private readonly IGlobalSettings _globalSettings; private readonly IHtmlSanitizer _htmlSanitizer; + private readonly IAppPolicyCache _cache; - public IconService(IGlobalSettings globalSettings, IHtmlSanitizer htmlSanitizer) + public IconService(IGlobalSettings globalSettings, IHtmlSanitizer htmlSanitizer, AppCaches appCaches) { _globalSettings = globalSettings; _htmlSanitizer = htmlSanitizer; + _cache = appCaches.RuntimeCache; } - /// - public IList GetAllIcons() - { - var directory = new DirectoryInfo(IOHelper.MapPath($"{_globalSettings.IconsPath}/")); - var iconNames = directory.GetFiles("*.svg"); + public IReadOnlyDictionary GetIcons() => GetIconDictionary(); - return iconNames.OrderBy(f => f.Name) - .Select(iconInfo => GetIcon(iconInfo)).WhereNotNull().ToList(); - - } + /// + public IList GetAllIcons() => + GetIconDictionary() + .Select(x => new IconModel { Name = x.Key, SvgString = x.Value }) + .ToList(); /// public IconModel GetIcon(string iconName) { - return string.IsNullOrWhiteSpace(iconName) - ? null - : CreateIconModel(iconName.StripFileExtension(), IOHelper.MapPath($"{_globalSettings.IconsPath}/{iconName}.svg")); + if (iconName.IsNullOrWhiteSpace()) + { + return null; + } + + var allIconModels = GetIconDictionary(); + if (allIconModels.ContainsKey(iconName)) + { + return new IconModel + { + Name = iconName, + SvgString = allIconModels[iconName] + }; + } + + return null; } /// @@ -79,5 +93,52 @@ namespace Umbraco.Web.Services return null; } } + + private IEnumerable GetAllIconsFiles() + { + var icons = new HashSet(new CaseInsensitiveFileInfoComparer()); + + // add icons from plugins + var appPluginsDirectoryPath = IOHelper.MapPath(SystemDirectories.AppPlugins); + if (Directory.Exists(appPluginsDirectoryPath)) + { + var appPlugins = new DirectoryInfo(appPluginsDirectoryPath); + + // iterate sub directories of app plugins + foreach (var dir in appPlugins.EnumerateDirectories()) + { + var iconPath = IOHelper.MapPath($"{SystemDirectories.AppPlugins}/{dir.Name}{SystemDirectories.AppPluginIcons}"); + if (Directory.Exists(iconPath)) + { + var dirIcons = new DirectoryInfo(iconPath).EnumerateFiles("*.svg", SearchOption.TopDirectoryOnly); + icons.UnionWith(dirIcons); + } + } + } + + // add icons from IconsPath if not already added from plugins + var coreIconsDirectory = new DirectoryInfo(IOHelper.MapPath($"{_globalSettings.IconsPath}/")); + var coreIcons = coreIconsDirectory.GetFiles("*.svg"); + + icons.UnionWith(coreIcons); + + return icons; + } + + private class CaseInsensitiveFileInfoComparer : IEqualityComparer + { + public bool Equals(FileInfo one, FileInfo two) => StringComparer.InvariantCultureIgnoreCase.Equals(one.Name, two.Name); + + public int GetHashCode(FileInfo item) => StringComparer.InvariantCultureIgnoreCase.GetHashCode(item.Name); + } + + private IReadOnlyDictionary GetIconDictionary() => _cache.GetCacheItem( + $"{typeof(IconService).FullName}.{nameof(GetIconDictionary)}", + () => GetAllIconsFiles() + .Select(GetIcon) + .Where(i => i != null) + .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First().SvgString, StringComparer.OrdinalIgnoreCase) + ); } }