diff --git a/src/UniGetUI.Core.IconStore/IconCacheEngine.cs b/src/UniGetUI.Core.IconStore/IconCacheEngine.cs index 3ce8c35300..da12b72894 100644 --- a/src/UniGetUI.Core.IconStore/IconCacheEngine.cs +++ b/src/UniGetUI.Core.IconStore/IconCacheEngine.cs @@ -24,6 +24,8 @@ public readonly struct CacheableIcon public readonly string Version = ""; public readonly long Size = -1; private readonly int _hashCode = -1; + public readonly bool IsLocalPath = false; + public readonly string LocalPath = ""; public readonly IconValidationMethod ValidationMethod; /// @@ -76,6 +78,13 @@ public CacheableIcon(Uri uri) _hashCode = uri.ToString().GetHashCode(); } + public CacheableIcon(string path) + { + IsLocalPath = true; + LocalPath = path; + Url = new Uri(path); + } + public override int GetHashCode() { return _hashCode; @@ -101,6 +110,10 @@ public static class IconCacheEngine return null; var icon = _icon.Value; + + if(icon.IsLocalPath) + return icon.LocalPath; + string iconLocation = Path.Join(CoreData.UniGetUICacheDirectory_Icons, ManagerName, PackageId); if (!Directory.Exists(iconLocation)) Directory.CreateDirectory(iconLocation); string iconVersionFile = Path.Join(iconLocation, $"icon.version"); diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/WinGetIconsHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/WinGetIconsHelper.cs new file mode 100644 index 0000000000..83dfa29b5c --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/WinGetIconsHelper.cs @@ -0,0 +1,178 @@ +using System.Globalization; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Management.Deployment; +using Microsoft.Win32; +using UniGetUI.Core.IconEngine; +using UniGetUI.Core.Logging; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Managers.WingetManager; + +namespace UniGetUI.PackageEngine.Managers.WinGet.ClientHelpers; +internal static class WinGetIconsHelper +{ + private static readonly Dictionary __msstore_package_manifests = []; + + public static string? GetMicrosoftStoreManifest(IPackage package) + { + if (__msstore_package_manifests.TryGetValue(package.Id, out var manifest)) + return manifest; + + string CountryCode = CultureInfo.CurrentCulture.Name.Split("-")[^1]; + string Locale = CultureInfo.CurrentCulture.Name; + string url = $"https://storeedgefd.dsx.mp.microsoft.com/v8.0/sdk/products?market={CountryCode}&locale={Locale}&deviceFamily=Windows.Desktop"; + +#pragma warning disable SYSLIB0014 + var httpRequest = (HttpWebRequest)WebRequest.Create(url); +#pragma warning restore SYSLIB0014 + + httpRequest.Method = "POST"; + httpRequest.ContentType = "application/json"; + + string data = "{\"productIds\": \"" + package.Id.ToLower() + "\"}"; + + using (StreamWriter streamWriter = new(httpRequest.GetRequestStream())) + streamWriter.Write(data); + + var httpResponse = httpRequest.GetResponse() as HttpWebResponse; + if (httpResponse is null) + { + Logger.Warn($"Null MS Store response for uri={url} and data={data}"); + return null; + } + + string result; + using (StreamReader streamReader = new(httpResponse.GetResponseStream())) + result = streamReader.ReadToEnd(); + + Logger.Debug("Microsoft Store API call status code: " + httpResponse.StatusCode); + + if (result != "" && httpResponse.StatusCode == HttpStatusCode.OK) + __msstore_package_manifests[package.Id] = result; + + return result; + } + + public static CacheableIcon? GetMicrosoftStoreIcon(IPackage package) + { + string? ResponseContent = GetMicrosoftStoreManifest(package); + if (ResponseContent is null) + return null; + + Match IconArray = Regex.Match(ResponseContent, "(?:\"|')Images(?:\"|'): ?\\[([^\\]]+)\\]"); + if (!IconArray.Success) + { + Logger.Warn("Could not parse Images array from Microsoft Store response"); + return null; + } + + Dictionary FoundIcons = []; + + foreach (Match ImageEntry in Regex.Matches(IconArray.Groups[1].Value, "{([^}]+)}")) + { + string CurrentImage = ImageEntry.Groups[1].Value; + + if (!ImageEntry.Success) + continue; + + Match ImagePurpose = Regex.Match(CurrentImage, "(?:\"|')ImagePurpose(?:\"|'): ?(?:\"|')([^'\"]+)(?:\"|')"); + if (!ImagePurpose.Success || ImagePurpose.Groups[1].Value != "Tile") + continue; + + Match ImageUrl = Regex.Match(CurrentImage, "(?:\"|')Uri(?:\"|'): ?(?:\"|')([^'\"]+)(?:\"|')"); + Match ImageSize = Regex.Match(CurrentImage, "(?:\"|')Height(?:\"|'): ?([^,]+)"); + + if (!ImageUrl.Success || !ImageSize.Success) + continue; + + FoundIcons[int.Parse(ImageSize.Groups[1].Value)] = ImageUrl.Groups[1].Value; + } + + if (FoundIcons.Count == 0) + { + Logger.Warn($"No Logo image found for package {package.Id} in Microsoft Store response"); + return null; + } + + Logger.Debug("Choosing icon with size " + FoundIcons.Keys.Max() + " for package " + package.Id + " from Microsoft Store"); + + string uri = "https:" + FoundIcons[FoundIcons.Keys.Max()]; + + return new CacheableIcon(new Uri(uri)); + } + + public static CacheableIcon? GetWinGetPackageIcon(IPackage package) + { + CatalogPackageMetadata? NativeDetails = NativePackageHandler.GetDetails(package); + if (NativeDetails is null) return null; + + // Get the actual icon and return it + foreach (Icon? icon in NativeDetails.Icons.ToArray()) + if (icon is not null && icon.Url is not null) + // Logger.Debug($"Found WinGet native icon for {package.Id} with URL={icon.Url}"); + return new CacheableIcon(new Uri(icon.Url), icon.Sha256); + + // Logger.Debug($"Native WinGet icon for Package={package.Id} on catalog={package.Source.Name} was not found :("); + return null; + } + + public static CacheableIcon? GetAPPXPackageIcon(IPackage package) + { + string appxId = package.Id.Replace("MSIX\\", ""); + + string globalPath; + var progsPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsApps", appxId); + if (Directory.Exists(progsPath)) + { + globalPath = Path.Join(progsPath, "Assets"); + if (!Directory.Exists(globalPath)) globalPath = Path.Join(progsPath, "Images"); + if (!Directory.Exists(globalPath)) globalPath = progsPath; + } + else + { + progsPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "SystemApps", appxId); + globalPath = Path.Join(progsPath, "Assets"); + if (!Directory.Exists(globalPath)) globalPath = Path.Join(progsPath, "Images"); + if (!Directory.Exists(globalPath)) globalPath = progsPath; + } + + if (!Directory.Exists(globalPath)) + return null; + + string[] logoFiles = Directory.GetFiles(globalPath, "*StoreLogo*.png", SearchOption.TopDirectoryOnly); + if (logoFiles.Length > 0) + return new CacheableIcon(logoFiles[^1]); + + logoFiles = Directory.GetFiles(globalPath, "*Splash*.png", SearchOption.TopDirectoryOnly); + if (logoFiles.Length > 0) + return new CacheableIcon(logoFiles[^1]); + + logoFiles = Directory.GetFiles(globalPath, "*.png", SearchOption.TopDirectoryOnly); + if (logoFiles.Length > 0) + return new CacheableIcon(logoFiles[^1]); + + return null; + } + + public static CacheableIcon? GetARPPackageIcon(IPackage package) + { + var bits = package.Id.Split("\\"); + if (bits.Length < 4) return null; + + string regKey = ""; + regKey += bits[1] == "Machine" ? "HKEY_LOCAL_MACHINE" : "HKEY_CURRENT_USER"; + regKey += "\\SOFTWARE"; + if (bits[2] == "X86") + regKey += "\\WOW6432Node"; + + regKey += "\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; + regKey += bits[3]; + + string? displayIcon = (string?)Registry.GetValue(regKey, "DisplayIcon", null); + if (!string.IsNullOrEmpty(displayIcon) && File.Exists(displayIcon) && !displayIcon.EndsWith(".exe")) + return new CacheableIcon(displayIcon); + + return null; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgDetailsHelper.cs index 67c59ed034..9b3a9b908f 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgDetailsHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgDetailsHelper.cs @@ -1,18 +1,16 @@ -using System.Globalization; -using System.Net; +using System.ComponentModel.Design; using System.Text.RegularExpressions; -using Microsoft.Management.Deployment; using UniGetUI.Core.IconEngine; using UniGetUI.Core.Logging; +using UniGetUI.Core.Tools; using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Managers.WinGet.ClientHelpers; namespace UniGetUI.PackageEngine.Managers.WingetManager { internal sealed class WinGetPkgDetailsHelper : BasePkgDetailsHelper { - private static readonly Dictionary __msstore_package_manifests = []; - public WinGetPkgDetailsHelper(WinGet manager) : base(manager) { } protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package) @@ -27,61 +25,21 @@ protected override void GetDetails_UnSafe(IPackageDetails details) protected override CacheableIcon? GetIcon_UnSafe(IPackage package) { - if (package.Source.IsVirtualManager) - return null; - - if (package.Source.Name == "msstore") - return GetMicrosoftStoreIcon(package); - - return GetWinGetPackageIcon(package); - } - - protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) - { - if (package.Source.Name != "msstore") + if (package.Source is LocalWinGetSource localSource) { - return []; - } + if(localSource.Type is LocalWinGetSource.Type_t.MicrosftStore) + return WinGetIconsHelper.GetAPPXPackageIcon(package); - string? ResponseContent = GetMicrosoftStoreManifest(package); - if (ResponseContent is null) - { - return []; - } + else if (localSource.Type is LocalWinGetSource.Type_t.LocalPC) + return WinGetIconsHelper.GetARPPackageIcon(package); - Match IconArray = Regex.Match(ResponseContent, "(?:\"|')Images(?:\"|'): ?\\[([^\\]]+)\\]"); - if (!IconArray.Success) - { - Logger.Warn("Could not parse Images array from Microsoft Store response"); - return []; + return null; } - List FoundIcons = []; - - foreach (Match ImageEntry in Regex.Matches(IconArray.Groups[1].Value, "{([^}]+)}")) - { + // if (package.Source.Name == "msstore") + // return WinGetIconsHelper.GetMicrosoftStoreIcon(package); - if (!ImageEntry.Success) - { - continue; - } - - Match ImagePurpose = Regex.Match(ImageEntry.Groups[1].Value, "(?:\"|')ImagePurpose(?:\"|'): ?(?:\"|')([^'\"]+)(?:\"|')"); - if (!ImagePurpose.Success || ImagePurpose.Groups[1].Value != "Screenshot") - { - continue; - } - - Match ImageUrl = Regex.Match(ImageEntry.Groups[1].Value, "(?:\"|')Uri(?:\"|'): ?(?:\"|')([^'\"]+)(?:\"|')"); - if (!ImageUrl.Success) - { - continue; - } - - FoundIcons.Add(new Uri("https:" + ImageUrl.Groups[1].Value)); - } - - return FoundIcons; + return WinGetIconsHelper.GetWinGetPackageIcon(package); } protected override string? GetInstallLocation_UnSafe(IPackage package) @@ -110,127 +68,53 @@ protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) return null; } - private static string? GetMicrosoftStoreManifest(IPackage package) + protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) { - if (__msstore_package_manifests.TryGetValue(package.Id, out var manifest)) - { - return manifest; - } - - string CountryCode = CultureInfo.CurrentCulture.Name.Split("-")[^1]; - string Locale = CultureInfo.CurrentCulture.Name; - string url = $"https://storeedgefd.dsx.mp.microsoft.com/v8.0/sdk/products?market={CountryCode}&locale={Locale}&deviceFamily=Windows.Desktop"; - -#pragma warning disable SYSLIB0014 - HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url); -#pragma warning restore SYSLIB0014 - - httpRequest.Method = "POST"; - httpRequest.ContentType = "application/json"; - - string data = "{\"productIds\": \"" + package.Id.ToLower() + "\"}"; - - using (StreamWriter streamWriter = new(httpRequest.GetRequestStream())) - { - streamWriter.Write(data); - } - - HttpWebResponse? httpResponse = httpRequest.GetResponse() as HttpWebResponse; - if (httpResponse is null) - { - Logger.Warn($"Null MS Store response for uri={url} and data={data}"); - return null; - } - - string result; - using (StreamReader streamReader = new(httpResponse.GetResponseStream())) - { - result = streamReader.ReadToEnd(); - } - - Logger.Debug("Microsoft Store API call status code: " + httpResponse.StatusCode); - - if (result != "" && httpResponse.StatusCode == HttpStatusCode.OK) + if (package.Source.Name != "msstore") { - __msstore_package_manifests[package.Id] = result; + return []; } - return result; - } - - private static CacheableIcon? GetMicrosoftStoreIcon(IPackage package) - { - string? ResponseContent = GetMicrosoftStoreManifest(package); + string? ResponseContent = WinGetIconsHelper.GetMicrosoftStoreManifest(package); if (ResponseContent is null) { - return null; + return []; } Match IconArray = Regex.Match(ResponseContent, "(?:\"|')Images(?:\"|'): ?\\[([^\\]]+)\\]"); if (!IconArray.Success) { Logger.Warn("Could not parse Images array from Microsoft Store response"); - return null; + return []; } - Dictionary FoundIcons = []; + List FoundIcons = []; foreach (Match ImageEntry in Regex.Matches(IconArray.Groups[1].Value, "{([^}]+)}")) { - string CurrentImage = ImageEntry.Groups[1].Value; if (!ImageEntry.Success) { continue; } - Match ImagePurpose = Regex.Match(CurrentImage, "(?:\"|')ImagePurpose(?:\"|'): ?(?:\"|')([^'\"]+)(?:\"|')"); - if (!ImagePurpose.Success || ImagePurpose.Groups[1].Value != "Tile") + Match ImagePurpose = Regex.Match(ImageEntry.Groups[1].Value, "(?:\"|')ImagePurpose(?:\"|'): ?(?:\"|')([^'\"]+)(?:\"|')"); + if (!ImagePurpose.Success || ImagePurpose.Groups[1].Value != "Screenshot") { continue; } - Match ImageUrl = Regex.Match(CurrentImage, "(?:\"|')Uri(?:\"|'): ?(?:\"|')([^'\"]+)(?:\"|')"); - Match ImageSize = Regex.Match(CurrentImage, "(?:\"|')Height(?:\"|'): ?([^,]+)"); - - if (!ImageUrl.Success || !ImageSize.Success) + Match ImageUrl = Regex.Match(ImageEntry.Groups[1].Value, "(?:\"|')Uri(?:\"|'): ?(?:\"|')([^'\"]+)(?:\"|')"); + if (!ImageUrl.Success) { continue; } - FoundIcons[int.Parse(ImageSize.Groups[1].Value)] = ImageUrl.Groups[1].Value; - } - - if (FoundIcons.Count == 0) - { - Logger.Warn($"No Logo image found for package {package.Id} in Microsoft Store response"); - return null; + FoundIcons.Add(new Uri("https:" + ImageUrl.Groups[1].Value)); } - Logger.Debug("Choosing icon with size " + FoundIcons.Keys.Max() + " for package " + package.Id + " from Microsoft Store"); - - string uri = "https:" + FoundIcons[FoundIcons.Keys.Max()]; - - return new CacheableIcon(new Uri(uri)); + return FoundIcons; } - private static CacheableIcon? GetWinGetPackageIcon(IPackage package) - { - CatalogPackageMetadata? NativeDetails = NativePackageHandler.GetDetails(package); - if (NativeDetails is null) return null; - - // Get the actual icon and return it - foreach (Icon? icon in NativeDetails.Icons.ToArray()) - { - if (icon is not null && icon.Url is not null) - { - // Logger.Debug($"Found WinGet native icon for {package.Id} with URL={icon.Url}"); - return new CacheableIcon(new Uri(icon.Url), icon.Sha256); - } - } - - // Logger.Debug($"Native WinGet icon for Package={package.Id} on catalog={package.Source.Name} was not found :("); - return null; - } } } diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs index 711015882f..36ac4fd108 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs @@ -81,12 +81,12 @@ public WinGet() DetailsHelper = new WinGetPkgDetailsHelper(this); OperationHelper = new WinGetPkgOperationHelper(this); - LocalPcSource = new LocalWinGetSource(this, CoreTools.Translate("Local PC"), IconType.LocalPc); - AndroidSubsystemSource = new(this, CoreTools.Translate("Android Subsystem"), IconType.Android); - SteamSource = new(this, "Steam", IconType.Steam); - UbisoftConnectSource = new(this, "Ubisoft Connect", IconType.UPlay); - GOGSource = new(this, "GOG", IconType.GOG); - MicrosoftStoreSource = new(this, "Microsoft Store", IconType.MsStore); + LocalPcSource = new LocalWinGetSource(this, CoreTools.Translate("Local PC"), IconType.LocalPc, LocalWinGetSource.Type_t.LocalPC); + AndroidSubsystemSource = new(this, CoreTools.Translate("Android Subsystem"), IconType.Android, LocalWinGetSource.Type_t.Android); + SteamSource = new(this, "Steam", IconType.Steam, LocalWinGetSource.Type_t.Steam); + UbisoftConnectSource = new(this, "Ubisoft Connect", IconType.UPlay, LocalWinGetSource.Type_t.Ubisoft); + GOGSource = new(this, "GOG", IconType.GOG, LocalWinGetSource.Type_t.GOG); + MicrosoftStoreSource = new(this, "Microsoft Store", IconType.MsStore, LocalWinGetSource.Type_t.MicrosftStore); } public static string GetProxyArgument() @@ -379,13 +379,25 @@ public override void RefreshPackageIndexes() public class LocalWinGetSource : ManagerSource { + public enum Type_t + { + LocalPC, + MicrosftStore, + Steam, + GOG, + Android, + Ubisoft + } + + public readonly Type_t Type; private readonly string name; private readonly IconType __icon_id; public override IconType IconId { get => __icon_id; } - public LocalWinGetSource(WinGet manager, string name, IconType iconId) + public LocalWinGetSource(WinGet manager, string name, IconType iconId, Type_t type) : base(manager, name, new Uri("https://microsoft.com/local-pc-source"), isVirtualManager: true) { + Type = type; this.name = name; __icon_id = iconId; AsString = Name; diff --git a/src/UniGetUI.PackageEngine.PackageLoader/DiscoverablePackagesLoader.cs b/src/UniGetUI.PackageEngine.PackageLoader/DiscoverablePackagesLoader.cs index 50f011c72f..8bdf02f41c 100644 --- a/src/UniGetUI.PackageEngine.PackageLoader/DiscoverablePackagesLoader.cs +++ b/src/UniGetUI.PackageEngine.PackageLoader/DiscoverablePackagesLoader.cs @@ -1,4 +1,3 @@ -using UniGetUI.Core.Logging; using UniGetUI.Core.Tools; using UniGetUI.Interface.Enums; using UniGetUI.PackageEngine.Interfaces; diff --git a/src/UniGetUI/App.xaml.cs b/src/UniGetUI/App.xaml.cs index f16529aed2..8a40a1a554 100644 --- a/src/UniGetUI/App.xaml.cs +++ b/src/UniGetUI/App.xaml.cs @@ -19,7 +19,6 @@ using UniGetUI.PackageEngine.Interfaces; using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs; using UniGetUI.Pages.DialogPages; -using UniGetUI.Interface.Enums; namespace UniGetUI { diff --git a/src/UniGetUI/Controls/LocalIcon.cs b/src/UniGetUI/Controls/LocalIcon.cs index 9c784aaabc..5df84f5fdb 100644 --- a/src/UniGetUI/Controls/LocalIcon.cs +++ b/src/UniGetUI/Controls/LocalIcon.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; diff --git a/src/UniGetUI/Pages/DialogPages/DialogHelper_Packages.cs b/src/UniGetUI/Pages/DialogPages/DialogHelper_Packages.cs index 5dc4b8034e..0242cd1f68 100644 --- a/src/UniGetUI/Pages/DialogPages/DialogHelper_Packages.cs +++ b/src/UniGetUI/Pages/DialogPages/DialogHelper_Packages.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.Tracing; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; @@ -11,7 +10,6 @@ using UniGetUI.PackageEngine.Interfaces; using UniGetUI.PackageEngine.PackageClasses; using UniGetUI.PackageEngine.Serializable; -using Windows.ApplicationModel; namespace UniGetUI.Pages.DialogPages;