diff --git a/DeviceDetector.NET.Icons.Tests/DeviceDetector.NET.Icons.Tests.csproj b/DeviceDetector.NET.Icons.Tests/DeviceDetector.NET.Icons.Tests.csproj new file mode 100644 index 0000000..0a01e64 --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/DeviceDetector.NET.Icons.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + false + DeviceDetectorNET.Icons.Tests + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/DeviceDetector.NET.Icons.Tests/IconResolverConstructionTests.cs b/DeviceDetector.NET.Icons.Tests/IconResolverConstructionTests.cs new file mode 100644 index 0000000..3e08eee --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/IconResolverConstructionTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using DeviceDetectorNET.Icons; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class IconResolverConstructionTests + { + [Fact] + public void ThrowsWhenOptionsIsNull() + { + Should.Throw(() => new IconResolver(null)); + } + + [Fact] + public void ThrowsWhenNeitherPhysicalRootPathNorFileExistsIsSet() + { + var options = new IconResolverOptions(); + + Should.Throw(() => new IconResolver(options)); + } + + [Fact] + public void ThrowsWhenExtensionPriorityIsEmpty() + { + var options = new IconResolverOptions + { + PhysicalRootPath = "C:\\icons", + ExtensionPriority = new List() + }; + + Should.Throw(() => new IconResolver(options)); + } + + [Fact] + public void DoesNotThrowWhenPhysicalRootPathIsSet() + { + var options = new IconResolverOptions { PhysicalRootPath = "C:\\icons" }; + + Should.NotThrow(() => new IconResolver(options)); + } + + [Fact] + public void DoesNotThrowWhenFileExistsDelegateIsSetWithoutPhysicalRootPath() + { + var options = new IconResolverOptions { FileExists = _ => false }; + + Should.NotThrow(() => new IconResolver(options)); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/IconResolverEndToEndTests.cs b/DeviceDetector.NET.Icons.Tests/IconResolverEndToEndTests.cs new file mode 100644 index 0000000..071d3ca --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/IconResolverEndToEndTests.cs @@ -0,0 +1,43 @@ +using DeviceDetectorNET.Icons; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class IconResolverEndToEndTests + { + [Fact] + public void ResolvesIconsForARealParsedUserAgent() + { + const string userAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"; + + var parseResult = DeviceDetectorNET.DeviceDetector.GetInfoFromUserAgent(userAgent); + parseResult.Success.ShouldBeTrue(); + + var resolver = new IconResolver(new IconResolverOptions { FileExists = _ => true }); + + var icons = resolver.GetIcons(parseResult.Match); + + icons.BotIcon.ShouldBeNull(); + icons.ClientIcon.ShouldContain("Chrome"); + icons.OsIcon.ShouldContain("Windows"); + } + + [Fact] + public void ResolvesBotIconForARealParsedBotUserAgent() + { + const string userAgent = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"; + + var parseResult = DeviceDetectorNET.DeviceDetector.GetInfoFromUserAgent(userAgent); + parseResult.Success.ShouldBeTrue(); + parseResult.Match.IsBoot.ShouldBeTrue(); + + var resolver = new IconResolver(new IconResolverOptions { FileExists = _ => true }); + + var icons = resolver.GetIcons(parseResult.Match); + + icons.BotIcon.ShouldContain("Googlebot"); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/IconResolverFallbackChainTests.cs b/DeviceDetector.NET.Icons.Tests/IconResolverFallbackChainTests.cs new file mode 100644 index 0000000..4583ca7 --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/IconResolverFallbackChainTests.cs @@ -0,0 +1,126 @@ +using DeviceDetectorNET.Icons; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class IconResolverFallbackChainTests + { + [Fact] + public void GetBotPrefersBotNameOverCategory() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("bot/Googlebot.svg"); + dir.CreateFile("bot/category/Search bot.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBot("Googlebot", "Search bot").ShouldBe("/assets/images/devicedetector/bot/Googlebot.svg"); + } + + [Fact] + public void GetBotFallsBackToCategoryWhenBotIconMissing() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("bot/category/Search bot.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBot("UnknownBot", "Search bot").ShouldBe("/assets/images/devicedetector/bot/category/Search bot.svg"); + } + + [Fact] + public void GetBotFallsBackToFallbackIconWhenNothingMatches() + { + using var dir = new TempIconDirectory(); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBot("UnknownBot", "UnknownCategory").ShouldBe("/assets/images/devicedetector/Matomo.svg"); + } + + [Fact] + public void GetBrowserFallsThroughFamilyThenEngineThenGenericBrowserIcon() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/browser/engine/Blink.svg"); + dir.CreateFile("client/type/browser.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBrowser("SomeNewBrowser", "SomeNewFamily", "Blink") + .ShouldBe("/assets/images/devicedetector/client/browser/engine/Blink.svg"); + } + + [Fact] + public void GetBrowserFallsBackToGenericBrowserIconWhenNothingElseMatches() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/type/browser.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBrowser("SomeNewBrowser", "SomeNewFamily", "SomeNewEngine") + .ShouldBe("/assets/images/devicedetector/client/type/browser.svg"); + } + + [Fact] + public void GetOsFallsThroughFamilyThenGenericOsIcon() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/type/os.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetOs("SomeNewOs", "SomeNewFamily").ShouldBe("/assets/images/devicedetector/client/type/os.svg"); + } + + [Fact] + public void GetClientUsesTypeSubfolderThenGenericTypeIcon() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/feed reader/Feedly.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetClient("Feedly", "feed reader") + .ShouldBe("/assets/images/devicedetector/client/feed reader/Feedly.svg"); + } + + [Fact] + public void GetClientFallsBackToGenericTypeIconWhenClientIconMissing() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/type/feed reader.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetClient("SomeUnknownReader", "feed reader") + .ShouldBe("/assets/images/devicedetector/client/type/feed reader.svg"); + } + + [Fact] + public void GetBrandPrefersBrandIconOverDeviceType() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("device/brand/Apple.svg"); + dir.CreateFile("device/type/smartphone.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBrand("Apple", "smartphone").ShouldBe("/assets/images/devicedetector/device/brand/Apple.svg"); + } + + [Fact] + public void GetBrandFallsBackToDeviceTypeIcon() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("device/type/smartphone.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBrand("SomeUnknownBrand", "smartphone") + .ShouldBe("/assets/images/devicedetector/device/type/smartphone.svg"); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/IconResolverFileExistsOverrideTests.cs b/DeviceDetector.NET.Icons.Tests/IconResolverFileExistsOverrideTests.cs new file mode 100644 index 0000000..b56ae67 --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/IconResolverFileExistsOverrideTests.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using DeviceDetectorNET.Icons; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class IconResolverFileExistsOverrideTests + { + [Fact] + public void UsesFileExistsDelegateInsteadOfPhysicalDisk() + { + var existingPaths = new HashSet { "/device/type/tv.svg" }; + + var resolver = new IconResolver(new IconResolverOptions + { + FileExists = existingPaths.Contains + }); + + resolver.GetDeviceType("tv").ShouldBe("/assets/images/devicedetector/device/type/tv.svg"); + resolver.GetDeviceType("desktop").ShouldBe("/assets/images/devicedetector/Matomo.svg"); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/IconResolverNameReplacementsTests.cs b/DeviceDetector.NET.Icons.Tests/IconResolverNameReplacementsTests.cs new file mode 100644 index 0000000..12ea4dc --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/IconResolverNameReplacementsTests.cs @@ -0,0 +1,37 @@ +using DeviceDetectorNET.Icons; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class IconResolverNameReplacementsTests + { + [Fact] + public void MergesCustomReplacementsOverDefaults() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/os/MyCustomSafeName.svg"); + + var options = new IconResolverOptions { PhysicalRootPath = dir.RootPath }; + options.NameReplacements["Custom/Unsafe:Name"] = "MyCustomSafeName"; + + var resolver = new IconResolver(options); + + resolver.GetOs("Custom/Unsafe:Name").ShouldBe("/assets/images/devicedetector/client/os/MyCustomSafeName.svg"); + } + + [Fact] + public void StillAppliesDefaultReplacementsWhenExtended() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/os/OS2.svg"); + + var options = new IconResolverOptions { PhysicalRootPath = dir.RootPath }; + options.NameReplacements["Another/Unsafe"] = "AnotherSafe"; + + var resolver = new IconResolver(options); + + resolver.GetOs("OS/2").ShouldBe("/assets/images/devicedetector/client/os/OS2.svg"); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/IconResolverOptionsTests.cs b/DeviceDetector.NET.Icons.Tests/IconResolverOptionsTests.cs new file mode 100644 index 0000000..da4e784 --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/IconResolverOptionsTests.cs @@ -0,0 +1,32 @@ +using DeviceDetectorNET.Icons; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class IconResolverOptionsTests + { + [Fact] + public void DefaultsMatchUpstreamDDCIcons() + { + var options = new IconResolverOptions(); + + options.UrlBasePath.ShouldBe("/assets/images/devicedetector"); + options.FallbackIconPath.ShouldBe("/Matomo.svg"); + options.ExtensionPriority.ShouldBe(new[] { "svg", "avif", "webp", "png", "jpg", "jpeg", "jxl", "heic", "gif" }); + options.NameReplacements.Count.ShouldBe(10); + options.NameReplacements["OS/2"].ShouldBe("OS2"); + options.NameReplacements["GNU/Linux"].ShouldBe("GNULinux"); + options.NameReplacements["MTK / Nucleus"].ShouldBe("MTK Nucleus"); + options.NameReplacements["Perl REST::Client"].ShouldBe("Perl RESTClient"); + options.NameReplacements["HTTP:Tiny"].ShouldBe("HTTP Tiny"); + options.NameReplacements["AUX"].ShouldBe("AUX"); + options.NameReplacements["MariaDB/MySQL Knowledge Base"].ShouldBe("MariaDB MySQL Knowledge Base"); + options.NameReplacements["Sandoba//Crawler"].ShouldBe("Sandoba Crawler"); + options.NameReplacements["WeSEE:Search"].ShouldBe("WeSEE Search"); + options.NameReplacements["Yeti/Naverbot"].ShouldBe("Yeti Naverbot"); + options.PhysicalRootPath.ShouldBeNull(); + options.FileExists.ShouldBeNull(); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/IconResolverResultTypeTests.cs b/DeviceDetector.NET.Icons.Tests/IconResolverResultTypeTests.cs new file mode 100644 index 0000000..758ba3d --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/IconResolverResultTypeTests.cs @@ -0,0 +1,149 @@ +using DeviceDetectorNET.Icons; +using DeviceDetectorNET.Parser.Device; +using DeviceDetectorNET.Results; +using DeviceDetectorNET.Results.Client; +using DeviceDetectorNET.Results.Device; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class IconResolverResultTypeTests + { + [Fact] + public void GetBotOverloadUsesNameThenCategory() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("bot/category/Search bot.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + var bot = new BotMatchResult { Name = "UnknownBot", Category = "Search bot" }; + + resolver.GetBot(bot).ShouldBe("/assets/images/devicedetector/bot/category/Search bot.svg"); + } + + [Fact] + public void GetBotOverloadHandlesNull() + { + using var dir = new TempIconDirectory(); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBot((BotMatchResult)null).ShouldBe("/assets/images/devicedetector/Matomo.svg"); + } + + [Fact] + public void GetBrowserOverloadUsesNameFamilyEngine() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/browser/family/Chrome.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + var browser = new BrowserMatchResult { Name = "Headless Chrome", Family = "Chrome", Engine = "Blink" }; + + resolver.GetBrowser(browser).ShouldBe("/assets/images/devicedetector/client/browser/family/Chrome.svg"); + } + + [Fact] + public void GetClientOverloadDispatchesToBrowserWhenClientIsBrowserMatchResult() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/browser/Vivaldi.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + ClientMatchResult client = new BrowserMatchResult { Name = "Vivaldi", Type = "browser" }; + + resolver.GetClient(client).ShouldBe("/assets/images/devicedetector/client/browser/Vivaldi.svg"); + } + + [Fact] + public void GetClientOverloadUsesGenericClientForNonBrowserTypes() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/feed reader/Feedly.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + var client = new ClientMatchResult { Name = "Feedly", Type = "feed reader" }; + + resolver.GetClient(client).ShouldBe("/assets/images/devicedetector/client/feed reader/Feedly.svg"); + } + + [Fact] + public void GetOsOverloadUsesNameThenFamily() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/os/family/Linux.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + var os = new OsMatchResult { Name = "Arch Linux", Family = "Linux" }; + + resolver.GetOs(os).ShouldBe("/assets/images/devicedetector/client/os/family/Linux.svg"); + } + + [Fact] + public void GetBrandOverloadResolvesDeviceTypeNameFromIntCode() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("device/type/smartphone.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + var device = new DeviceMatchResult { Brand = "SomeUnknownBrand", Type = DeviceType.DEVICE_TYPE_SMARTPHONE }; + + resolver.GetBrand(device).ShouldBe("/assets/images/devicedetector/device/type/smartphone.svg"); + } + + [Fact] + public void GetDeviceTypeOverloadResolvesNameFromIntCode() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("device/type/smartphone.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetDeviceType((int?)DeviceType.DEVICE_TYPE_SMARTPHONE) + .ShouldBe("/assets/images/devicedetector/device/type/smartphone.svg"); + } + + [Fact] + public void GetIconsIncludesBotIconWhenResultIsBot() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("bot/category/Search bot.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + var result = new DeviceDetectorResult + { + Bot = new BotMatchResult { Name = "UnknownBot", Category = "Search bot" } + }; + + var icons = resolver.GetIcons(result); + + icons.BotIcon.ShouldBe("/assets/images/devicedetector/bot/category/Search bot.svg"); + } + + [Fact] + public void GetIconsBundlesAllFivePaths() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/type/browser.svg"); + dir.CreateFile("client/type/os.svg"); + dir.CreateFile("device/type/tv.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + var result = new DeviceDetectorResult + { + Os = new OsMatchResult { Name = "SomeUnknownOs" }, + Client = new BrowserMatchResult { Name = "SomeUnknownBrowser", Type = "browser" }, + Device = new DeviceMatchResult { Brand = "SomeUnknownBrand", Type = DeviceType.DEVICE_TYPE_TV } + }; + + var icons = resolver.GetIcons(result); + + icons.BotIcon.ShouldBeNull(); + icons.OsIcon.ShouldBe("/assets/images/devicedetector/client/type/os.svg"); + icons.ClientIcon.ShouldBe("/assets/images/devicedetector/client/type/browser.svg"); + icons.BrandIcon.ShouldBe("/assets/images/devicedetector/device/type/tv.svg"); + icons.DeviceTypeIcon.ShouldBe("/assets/images/devicedetector/device/type/tv.svg"); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/IconResolverSingleLevelTests.cs b/DeviceDetector.NET.Icons.Tests/IconResolverSingleLevelTests.cs new file mode 100644 index 0000000..7c7c03c --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/IconResolverSingleLevelTests.cs @@ -0,0 +1,101 @@ +using DeviceDetectorNET.Icons; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class IconResolverSingleLevelTests + { + [Fact] + public void PrefersHigherPriorityExtensionWhenMultipleExist() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/type/browser.png"); + dir.CreateFile("client/type/browser.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetClientType("browser").ShouldBe("/assets/images/devicedetector/client/type/browser.svg"); + } + + [Fact] + public void FallsBackToConfiguredFallbackIconWhenNothingMatches() + { + using var dir = new TempIconDirectory(); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetClientType("nonexistent-type").ShouldBe("/assets/images/devicedetector/Matomo.svg"); + } + + [Fact] + public void SanitizesNameBeforeLookingUpFile() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/os/OS2.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetOs("OS/2").ShouldBe("/assets/images/devicedetector/client/os/OS2.svg"); + } + + [Fact] + public void UsesCustomUrlBasePath() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("device/type/tv.svg"); + + var resolver = new IconResolver(new IconResolverOptions + { + PhysicalRootPath = dir.RootPath, + UrlBasePath = "/cdn/icons" + }); + + resolver.GetDeviceType("tv").ShouldBe("/cdn/icons/device/type/tv.svg"); + } + + [Fact] + public void GetBotCategoryResolvesDirectly() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("bot/category/Crawler.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBotCategory("Crawler").ShouldBe("/assets/images/devicedetector/bot/category/Crawler.svg"); + } + + [Fact] + public void GetBrowserFamilyResolvesDirectly() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/browser/family/Chrome.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBrowserFamily("Chrome").ShouldBe("/assets/images/devicedetector/client/browser/family/Chrome.svg"); + } + + [Fact] + public void GetBrowserEngineResolvesDirectly() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/browser/engine/Blink.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetBrowserEngine("Blink").ShouldBe("/assets/images/devicedetector/client/browser/engine/Blink.svg"); + } + + [Fact] + public void GetOsFamilyResolvesDirectly() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("client/os/family/Linux.svg"); + + var resolver = new IconResolver(new IconResolverOptions { PhysicalRootPath = dir.RootPath }); + + resolver.GetOsFamily("Linux").ShouldBe("/assets/images/devicedetector/client/os/family/Linux.svg"); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/ServiceCollectionExtensionsTests.cs b/DeviceDetector.NET.Icons.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..282f8ea --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,43 @@ +using System; +using DeviceDetectorNET.Icons; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace DeviceDetectorNET.Icons.Tests +{ + public class ServiceCollectionExtensionsTests + { + [Fact] + public void RegistersIconResolverAsSingletonWithConfiguredOptions() + { + using var dir = new TempIconDirectory(); + dir.CreateFile("device/type/tv.svg"); + + var services = new ServiceCollection(); + services.AddDeviceDetectorIcons(options => options.PhysicalRootPath = dir.RootPath); + + using var provider = services.BuildServiceProvider(); + var resolver = provider.GetRequiredService(); + + resolver.GetDeviceType("tv").ShouldBe("/assets/images/devicedetector/device/type/tv.svg"); + provider.GetRequiredService().ShouldBeSameAs(resolver); + } + + [Fact] + public void ThrowsWhenServicesIsNull() + { + Should.Throw(() => + ServiceCollectionExtensions.AddDeviceDetectorIcons(null, options => { })); + } + + [Fact] + public void ThrowsWhenConfigureOptionsIsNull() + { + var services = new ServiceCollection(); + + Should.Throw(() => + services.AddDeviceDetectorIcons(null)); + } + } +} diff --git a/DeviceDetector.NET.Icons.Tests/TestHelpers/TempIconDirectory.cs b/DeviceDetector.NET.Icons.Tests/TestHelpers/TempIconDirectory.cs new file mode 100644 index 0000000..196ab9d --- /dev/null +++ b/DeviceDetector.NET.Icons.Tests/TestHelpers/TempIconDirectory.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; + +namespace DeviceDetectorNET.Icons.Tests +{ + /// + /// Creates an isolated temp directory that mirrors the icon pack's folder layout for a test, + /// and deletes it on dispose. + /// + internal sealed class TempIconDirectory : IDisposable + { + public TempIconDirectory() + { + RootPath = Path.Combine(Path.GetTempPath(), "DeviceDetectorIconsTests_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(RootPath); + } + + public string RootPath { get; } + + public void CreateFile(string relativePath) + { + var fullPath = Path.Combine(RootPath, relativePath.TrimStart('/', '\\').Replace('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + File.WriteAllText(fullPath, string.Empty); + } + + public void Dispose() + { + if (Directory.Exists(RootPath)) + { + Directory.Delete(RootPath, recursive: true); + } + } + } +} diff --git a/DeviceDetector.NET.Icons/DeviceDetector.NET.Icons.csproj b/DeviceDetector.NET.Icons/DeviceDetector.NET.Icons.csproj new file mode 100644 index 0000000..85d2261 --- /dev/null +++ b/DeviceDetector.NET.Icons/DeviceDetector.NET.Icons.csproj @@ -0,0 +1,41 @@ + + + + netstandard2.0;net462;net8.0;net9.0;net10.0 + DeviceDetectorNET.Icons + 1.0.0 + totpero + Apache-2.0 + Copyright © www.totpe.ro + Resolves icon file paths for DeviceDetector.NET detection results (bots, browsers, operating systems, clients and device brands/types), with filesystem-existence-based extension fallback. The resolution algorithm is ported from Simbiat/DeviceDetectorIcons (MIT licensed). This package does not bundle icon image assets; consumers supply their own icon files. + device-detector icons user-agent bot-detection + https://github.com/totpero/DeviceDetector.NET + https://github.com/totpero/DeviceDetector.NET + git + 1.0.0.0 + 1.0.0.0 + false + logo.jpg + README.md + true + true + portable + snupkg + true + + + + + + + + + + + + + + + + + diff --git a/DeviceDetector.NET.Icons/DeviceIconPaths.cs b/DeviceDetector.NET.Icons/DeviceIconPaths.cs new file mode 100644 index 0000000..06aade9 --- /dev/null +++ b/DeviceDetector.NET.Icons/DeviceIconPaths.cs @@ -0,0 +1,24 @@ +namespace DeviceDetectorNET.Icons +{ + /// + /// Bundles the icon paths for every part of a . + /// + public sealed class DeviceIconPaths + { + public DeviceIconPaths(string botIcon, string osIcon, string clientIcon, string brandIcon, string deviceTypeIcon) + { + BotIcon = botIcon; + OsIcon = osIcon; + ClientIcon = clientIcon; + BrandIcon = brandIcon; + DeviceTypeIcon = deviceTypeIcon; + } + + /// Null when the result did not match a bot. + public string BotIcon { get; } + public string OsIcon { get; } + public string ClientIcon { get; } + public string BrandIcon { get; } + public string DeviceTypeIcon { get; } + } +} diff --git a/DeviceDetector.NET.Icons/IconResolver.ResultTypes.cs b/DeviceDetector.NET.Icons/IconResolver.ResultTypes.cs new file mode 100644 index 0000000..a06dfbd --- /dev/null +++ b/DeviceDetector.NET.Icons/IconResolver.ResultTypes.cs @@ -0,0 +1,76 @@ +using System; +using DeviceDetectorNET.Parser.Device; +using DeviceDetectorNET.Results; +using DeviceDetectorNET.Results.Client; +using DeviceDetectorNET.Results.Device; + +namespace DeviceDetectorNET.Icons +{ + public partial class IconResolver + { + public string GetBot(BotMatchResult bot) + { + return bot == null + ? GetBot(string.Empty, string.Empty) + : GetBot(bot.Name, bot.Category); + } + + public string GetBrowser(BrowserMatchResult browser) + { + return browser == null + ? GetBrowser(string.Empty, string.Empty, string.Empty) + : GetBrowser(browser.Name, browser.Family, browser.Engine); + } + + public string GetClient(ClientMatchResult client) + { + if (client == null) + { + return GetClient(string.Empty, string.Empty); + } + + return client is BrowserMatchResult browserMatch + ? GetBrowser(browserMatch) + : GetClient(client.Name, client.Type); + } + + public string GetOs(OsMatchResult os) + { + return os == null + ? GetOs(string.Empty, string.Empty) + : GetOs(os.Name, os.Family); + } + + public string GetBrand(DeviceMatchResult device) + { + if (device == null) + { + return GetBrand(string.Empty, string.Empty); + } + + var deviceTypeName = device.Type.HasValue ? Devices.GetDeviceName(device.Type.Value) : string.Empty; + return GetBrand(device.Brand, deviceTypeName); + } + + public string GetDeviceType(int? deviceType) + { + var deviceTypeName = deviceType.HasValue ? Devices.GetDeviceName(deviceType.Value) : string.Empty; + return GetDeviceType(deviceTypeName); + } + + public DeviceIconPaths GetIcons(DeviceDetectorResult result) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + return new DeviceIconPaths( + botIcon: result.IsBoot ? GetBot(result.Bot) : null, + osIcon: GetOs(result.Os), + clientIcon: GetClient(result.Client), + brandIcon: GetBrand(result.Device), + deviceTypeIcon: GetDeviceType(result.Device != null ? result.Device.Type : null)); + } + } +} diff --git a/DeviceDetector.NET.Icons/IconResolver.cs b/DeviceDetector.NET.Icons/IconResolver.cs new file mode 100644 index 0000000..fec168c --- /dev/null +++ b/DeviceDetector.NET.Icons/IconResolver.cs @@ -0,0 +1,161 @@ +using System; +using System.IO; + +namespace DeviceDetectorNET.Icons +{ + /// + /// Resolves icon file paths for DeviceDetector.NET detection results. Ported from the + /// resolution logic of Simbiat/DeviceDetectorIcons (MIT licensed, © 2021 Dmitrii Kustov). + /// + public partial class IconResolver + { + private const string BotPath = "/bot"; + private const string BotCategoryPath = "/bot/category"; + private const string ClientRootPath = "/client"; + private const string ClientTypePath = "/client/type"; + private const string BrowserPath = "/client/browser"; + private const string BrowserFamilyPath = "/client/browser/family"; + private const string BrowserEnginePath = "/client/browser/engine"; + private const string OsPath = "/client/os"; + private const string OsFamilyPath = "/client/os/family"; + private const string BrandPath = "/device/brand"; + private const string DeviceTypePath = "/device/type"; + + private readonly IconResolverOptions _options; + private readonly Func _fileExists; + + public IconResolver(IconResolverOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.FileExists == null && string.IsNullOrEmpty(options.PhysicalRootPath)) + { + throw new ArgumentException("Either PhysicalRootPath or FileExists must be set.", nameof(options)); + } + + if (options.ExtensionPriority == null || options.ExtensionPriority.Count == 0) + { + throw new ArgumentException("ExtensionPriority must contain at least one extension.", nameof(options)); + } + + _options = options; + _fileExists = options.FileExists ?? DefaultFileExists; + } + + private bool DefaultFileExists(string relativePath) + { + var normalized = relativePath.TrimStart('/', '\\').Replace('/', Path.DirectorySeparatorChar); + return File.Exists(Path.Combine(_options.PhysicalRootPath, normalized)); + } + + private string SanitizeName(string name) + { + var sanitized = name ?? string.Empty; + if (_options.NameReplacements == null) + { + return sanitized; + } + + foreach (var replacement in _options.NameReplacements) + { + sanitized = sanitized.Replace(replacement.Key, replacement.Value); + } + + return sanitized; + } + + private string ResolveIcon(string name, string relativeFolder) + { + if (string.IsNullOrEmpty(name)) + { + return null; + } + + var sanitized = SanitizeName(name); + + foreach (var extension in _options.ExtensionPriority) + { + var relativePath = relativeFolder + "/" + sanitized + "." + extension; + if (_fileExists(relativePath)) + { + return relativePath; + } + } + + return null; + } + + private string WithUrlBasePath(string relativePath) + { + return _options.UrlBasePath + (relativePath ?? _options.FallbackIconPath); + } + + public string GetBot(string bot, string category = null) + { + var icon = ResolveIcon(bot, BotPath) ?? ResolveIcon(category, BotCategoryPath); + return WithUrlBasePath(icon); + } + + public string GetBotCategory(string category) + { + return WithUrlBasePath(ResolveIcon(category, BotCategoryPath)); + } + + public string GetBrowser(string browser, string family = null, string engine = null) + { + var icon = ResolveIcon(browser, BrowserPath) + ?? ResolveIcon(family, BrowserFamilyPath) + ?? ResolveIcon(engine, BrowserEnginePath) + ?? ResolveIcon("browser", ClientTypePath); + return WithUrlBasePath(icon); + } + + public string GetBrowserFamily(string family) + { + return WithUrlBasePath(ResolveIcon(family, BrowserFamilyPath)); + } + + public string GetBrowserEngine(string engine) + { + return WithUrlBasePath(ResolveIcon(engine, BrowserEnginePath)); + } + + public string GetOs(string os, string family = null) + { + var icon = ResolveIcon(os, OsPath) + ?? ResolveIcon(family, OsFamilyPath) + ?? ResolveIcon("os", ClientTypePath); + return WithUrlBasePath(icon); + } + + public string GetOsFamily(string family) + { + return WithUrlBasePath(ResolveIcon(family, OsFamilyPath)); + } + + public string GetClient(string client, string type) + { + var icon = ResolveIcon(client, ClientRootPath + "/" + type) ?? ResolveIcon(type, ClientTypePath); + return WithUrlBasePath(icon); + } + + public string GetClientType(string type) + { + return WithUrlBasePath(ResolveIcon(type, ClientTypePath)); + } + + public string GetBrand(string brand, string type = null) + { + var icon = ResolveIcon(brand, BrandPath) ?? ResolveIcon(type, DeviceTypePath); + return WithUrlBasePath(icon); + } + + public string GetDeviceType(string type) + { + return WithUrlBasePath(ResolveIcon(type, DeviceTypePath)); + } + } +} diff --git a/DeviceDetector.NET.Icons/IconResolverOptions.cs b/DeviceDetector.NET.Icons/IconResolverOptions.cs new file mode 100644 index 0000000..2187ba4 --- /dev/null +++ b/DeviceDetector.NET.Icons/IconResolverOptions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; + +namespace DeviceDetectorNET.Icons +{ + /// + /// Configuration for . + /// + public class IconResolverOptions + { + public IconResolverOptions() + { + UrlBasePath = "/assets/images/devicedetector"; + FallbackIconPath = "/Matomo.svg"; + ExtensionPriority = new List { "svg", "avif", "webp", "png", "jpg", "jpeg", "jxl", "heic", "gif" }; + NameReplacements = new Dictionary + { + ["OS/2"] = "OS2", + ["GNU/Linux"] = "GNULinux", + ["MTK / Nucleus"] = "MTK Nucleus", + ["Perl REST::Client"] = "Perl RESTClient", + ["HTTP:Tiny"] = "HTTP Tiny", + ["AUX"] = "AUX", + ["MariaDB/MySQL Knowledge Base"] = "MariaDB MySQL Knowledge Base", + ["Sandoba//Crawler"] = "Sandoba Crawler", + ["WeSEE:Search"] = "WeSEE Search", + ["Yeti/Naverbot"] = "Yeti Naverbot" + }; + } + + /// + /// Folder that directly contains the icon pack's bot/, client/ and device/ + /// subfolders. Required unless is set. + /// + public string PhysicalRootPath { get; set; } + + /// + /// Prefix prepended to every path returned by . + /// + public string UrlBasePath { get; set; } + + /// + /// Path (with prefix) returned when nothing in a fallback chain resolves. + /// + public string FallbackIconPath { get; set; } + + /// + /// File extensions to probe, in priority order. + /// + public IList ExtensionPriority { get; set; } + + /// + /// Filesystem-unsafe name substitutions applied before probing for a file. + /// + public IDictionary NameReplacements { get; set; } + + /// + /// Overrides the existence check used instead of File.Exists against + /// — e.g. to back it with an IFileProvider. + /// + public Func FileExists { get; set; } + } +} diff --git a/DeviceDetector.NET.Icons/README.md b/DeviceDetector.NET.Icons/README.md new file mode 100644 index 0000000..09e233f --- /dev/null +++ b/DeviceDetector.NET.Icons/README.md @@ -0,0 +1,90 @@ +DeviceDetector.NET.Icons +========================= + +Resolves icon file paths for [DeviceDetector.NET](https://github.com/totpero/DeviceDetector.NET) +detection results — bots, browsers, operating systems, clients (feed readers, media players, PIMs, +libraries, mobile apps) and device brands/types — with filesystem-existence-based extension fallback. + +This package does **not** bundle icon image assets. Point it at your own copy of an icon pack, such as: +- Unofficial [Simbiat/DeviceDetectorIcons](https://github.com/Simbiat/DeviceDetectorIcons) pack (the + resolution algorithm below is a C# port of this project's `DDCIcons.php`, MIT licensed, + © 2021 Dmitrii Kustov — see [Attribution](#attribution)) +- Official [Matomo icons](https://github.com/matomo-org/matomo-icons/) pack + +## Install + +``` +dotnet add package DeviceDetector.NET.Icons +``` + +## Usage + +```csharp +using DeviceDetectorNET; +using DeviceDetectorNET.Icons; + +var resolver = new IconResolver(new IconResolverOptions +{ + // Folder that directly contains the icon pack's bot/, client/ and device/ subfolders. + PhysicalRootPath = "/var/www/wwwroot/assets/images/devicedetector" +}); + +var parseResult = DeviceDetector.GetInfoFromUserAgent(userAgentString); +var icons = resolver.GetIcons(parseResult.Match); + +// icons.BotIcon, icons.OsIcon, icons.ClientIcon, icons.BrandIcon, icons.DeviceTypeIcon +``` + +Or register it for dependency injection: + +```csharp +using DeviceDetectorNET.Icons; + +builder.Services.AddDeviceDetectorIcons(options => +{ + options.PhysicalRootPath = Path.Combine(builder.Environment.WebRootPath, "assets/images/devicedetector"); +}); +``` + +Individual lookups are also available, mirroring the upstream PHP API: + +```csharp +resolver.GetBrowser("Chrome", family: "Chrome", engine: "Blink"); +resolver.GetOs("Ubuntu", family: "Linux"); +resolver.GetBrand("Apple", type: "smartphone"); +``` + +### Hosting icons behind an `IFileProvider` or CDN + +By default, existence is checked with `File.Exists` against `PhysicalRootPath`. To back it with +something else (an ASP.NET Core `IFileProvider`, a CDN manifest, etc.), set `FileExists` instead: + +```csharp +var resolver = new IconResolver(new IconResolverOptions +{ + FileExists = relativePath => fileProvider.GetFileInfo(relativePath).Exists, + UrlBasePath = "https://cdn.example.com/devicedetector-icons" +}); +``` + +## Attribution + +The path-resolution, extension-priority-fallback and name-sanitization logic in this package is a +C# port of [`Simbiat/DeviceDetectorIcons`](https://github.com/Simbiat/DeviceDetectorIcons)'s +`DDCIcons.php` class: + +> MIT License +> +> Copyright (c) 2021 Dmitrii Kustov +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +> associated documentation files (the "Software"), to deal in the Software without restriction, +> including without limitation the rights to use, copy, modify, merge, publish, distribute, +> sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: The above copyright notice and this +> permission notice shall be included in all copies or substantial portions of the Software. THE +> SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +> LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +> DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +> OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/DeviceDetector.NET.Icons/ServiceCollectionExtensions.cs b/DeviceDetector.NET.Icons/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..31a1b67 --- /dev/null +++ b/DeviceDetector.NET.Icons/ServiceCollectionExtensions.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace DeviceDetectorNET.Icons +{ + public static class ServiceCollectionExtensions + { + /// + /// Registers a singleton configured by . + /// + public static IServiceCollection AddDeviceDetectorIcons(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + var options = new IconResolverOptions(); + configureOptions(options); + + services.AddSingleton(new IconResolver(options)); + return services; + } + } +} diff --git a/DeviceDetector.NET.sln b/DeviceDetector.NET.sln index d886762..6804dd1 100644 --- a/DeviceDetector.NET.sln +++ b/DeviceDetector.NET.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11109.219 d18.0-oob +VisualStudioVersion = 18.0.11109.219 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeviceDetector.NET", "DeviceDetector.NET\DeviceDetector.NET.csproj", "{9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}" EndProject @@ -34,44 +34,132 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Enrichers.AspNetCor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Enrichers.AspNetCore.DeviceDetector.Tests", "Serilog.Enrichers.AspNetCore.DeviceDetector.Tests\Serilog.Enrichers.AspNetCore.DeviceDetector.Tests.csproj", "{6871B4FE-BC7F-4B63-9BE0-31489F685C70}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceDetector.NET.Icons", "DeviceDetector.NET.Icons\DeviceDetector.NET.Icons.csproj", "{DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceDetector.NET.Icons.Tests", "DeviceDetector.NET.Icons.Tests\DeviceDetector.NET.Icons.Tests.csproj", "{FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Debug|x64.Build.0 = Debug|Any CPU + {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Debug|x86.Build.0 = Debug|Any CPU {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Release|Any CPU.Build.0 = Release|Any CPU + {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Release|x64.ActiveCfg = Release|Any CPU + {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Release|x64.Build.0 = Release|Any CPU + {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Release|x86.ActiveCfg = Release|Any CPU + {9D04EA3B-96FC-4B15-AA5E-8F6E6AD31E9C}.Release|x86.Build.0 = Release|Any CPU {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Debug|x64.ActiveCfg = Debug|Any CPU + {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Debug|x64.Build.0 = Debug|Any CPU + {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Debug|x86.ActiveCfg = Debug|Any CPU + {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Debug|x86.Build.0 = Debug|Any CPU {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Release|Any CPU.ActiveCfg = Release|Any CPU {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Release|Any CPU.Build.0 = Release|Any CPU + {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Release|x64.ActiveCfg = Release|Any CPU + {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Release|x64.Build.0 = Release|Any CPU + {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Release|x86.ActiveCfg = Release|Any CPU + {24AF45AC-9BF3-46FB-85A3-2FA3C6EDB44A}.Release|x86.Build.0 = Release|Any CPU {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Debug|x64.ActiveCfg = Debug|Any CPU + {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Debug|x64.Build.0 = Debug|Any CPU + {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Debug|x86.ActiveCfg = Debug|Any CPU + {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Debug|x86.Build.0 = Debug|Any CPU {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Release|Any CPU.ActiveCfg = Release|Any CPU {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Release|Any CPU.Build.0 = Release|Any CPU + {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Release|x64.ActiveCfg = Release|Any CPU + {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Release|x64.Build.0 = Release|Any CPU + {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Release|x86.ActiveCfg = Release|Any CPU + {94B4F635-61AC-40E4-87CC-DDA4217A0999}.Release|x86.Build.0 = Release|Any CPU {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Debug|x64.Build.0 = Debug|Any CPU + {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Debug|x86.Build.0 = Debug|Any CPU {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Release|Any CPU.Build.0 = Release|Any CPU + {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Release|x64.ActiveCfg = Release|Any CPU + {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Release|x64.Build.0 = Release|Any CPU + {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Release|x86.ActiveCfg = Release|Any CPU + {E4DA874C-7C29-4D34-9C0E-103073AEE043}.Release|x86.Build.0 = Release|Any CPU {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Debug|x64.ActiveCfg = Debug|Any CPU + {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Debug|x64.Build.0 = Debug|Any CPU + {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Debug|x86.ActiveCfg = Debug|Any CPU + {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Debug|x86.Build.0 = Debug|Any CPU {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Release|Any CPU.ActiveCfg = Release|Any CPU {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Release|Any CPU.Build.0 = Release|Any CPU + {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Release|x64.ActiveCfg = Release|Any CPU + {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Release|x64.Build.0 = Release|Any CPU + {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Release|x86.ActiveCfg = Release|Any CPU + {04D3C595-50EF-4E7B-8DF6-D5F997F4446D}.Release|x86.Build.0 = Release|Any CPU {F88615DF-0E16-460A-814E-A288889DE179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F88615DF-0E16-460A-814E-A288889DE179}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F88615DF-0E16-460A-814E-A288889DE179}.Debug|x64.ActiveCfg = Debug|Any CPU + {F88615DF-0E16-460A-814E-A288889DE179}.Debug|x64.Build.0 = Debug|Any CPU + {F88615DF-0E16-460A-814E-A288889DE179}.Debug|x86.ActiveCfg = Debug|Any CPU + {F88615DF-0E16-460A-814E-A288889DE179}.Debug|x86.Build.0 = Debug|Any CPU {F88615DF-0E16-460A-814E-A288889DE179}.Release|Any CPU.ActiveCfg = Release|Any CPU {F88615DF-0E16-460A-814E-A288889DE179}.Release|Any CPU.Build.0 = Release|Any CPU + {F88615DF-0E16-460A-814E-A288889DE179}.Release|x64.ActiveCfg = Release|Any CPU + {F88615DF-0E16-460A-814E-A288889DE179}.Release|x64.Build.0 = Release|Any CPU + {F88615DF-0E16-460A-814E-A288889DE179}.Release|x86.ActiveCfg = Release|Any CPU + {F88615DF-0E16-460A-814E-A288889DE179}.Release|x86.Build.0 = Release|Any CPU {2C6A9F5E-3B3D-4B0A-9E3F-9B4C5D6E7F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2C6A9F5E-3B3D-4B0A-9E3F-9B4C5D6E7F80}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C6A9F5E-3B3D-4B0A-9E3F-9B4C5D6E7F80}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C6A9F5E-3B3D-4B0A-9E3F-9B4C5D6E7F80}.Release|Any CPU.Build.0 = Release|Any CPU {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Debug|x64.ActiveCfg = Debug|Any CPU + {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Debug|x64.Build.0 = Debug|Any CPU + {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Debug|x86.ActiveCfg = Debug|Any CPU + {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Debug|x86.Build.0 = Debug|Any CPU {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Release|Any CPU.ActiveCfg = Release|Any CPU {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Release|Any CPU.Build.0 = Release|Any CPU + {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Release|x64.ActiveCfg = Release|Any CPU + {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Release|x64.Build.0 = Release|Any CPU + {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Release|x86.ActiveCfg = Release|Any CPU + {6871B4FE-BC7F-4B63-9BE0-31489F685C70}.Release|x86.Build.0 = Release|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Debug|x64.Build.0 = Debug|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Debug|x86.Build.0 = Debug|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Release|Any CPU.Build.0 = Release|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Release|x64.ActiveCfg = Release|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Release|x64.Build.0 = Release|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Release|x86.ActiveCfg = Release|Any CPU + {DF4201EA-0991-4A3B-AE89-DB2E6ABB8E9C}.Release|x86.Build.0 = Release|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Debug|x64.Build.0 = Debug|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Debug|x86.Build.0 = Debug|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Release|Any CPU.Build.0 = Release|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Release|x64.ActiveCfg = Release|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Release|x64.Build.0 = Release|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Release|x86.ActiveCfg = Release|Any CPU + {FF6F8F1A-735D-42C7-A41C-53FCA8C552A3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 25afd20..8026d09 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,9 @@ There are already a few ports of this tool to other languages: If you are looking for icons to use alongside Device Detector, these repositories can be of use: - Official [Matomo](https://github.com/matomo-org/matomo-icons/) pack - Unofficial [Simbiat](https://github.com/Simbiat/DeviceDetectorIcons) pack -- -..... + +If you're using .NET, [`DeviceDetector.NET.Icons`](DeviceDetector.NET.Icons/README.md) resolves icon +file paths for detection results from any of the packs above (it does not bundle icon assets itself). The lists below are auto generated and updated from time to time. Some of them might not be complete.