diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml new file mode 100644 index 0000000..caac10f --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml.cs new file mode 100644 index 0000000..d5ab1cc --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml.cs @@ -0,0 +1,87 @@ +// Copyright © 2017-2025 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using QuickLook.Common.ExtensionMethods; +using QuickLook.Common.Helpers; +using QuickLook.Plugin.AppViewer.AppxPackageParser; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Media.Imaging; + +namespace QuickLook.Plugin.AppViewer; + +public partial class AppxInfoPanel : UserControl, IAppInfoPanel +{ + public AppxInfoPanel() + { + DataContext = this; + InitializeComponent(); + + string translationFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations.config"); + productNameTitle.Text = TranslationHelper.Get("PRODUCT_NAME", translationFile); + productVersionTitle.Text = TranslationHelper.Get("PRODUCT_VERSION", translationFile); + publisherTitle.Text = TranslationHelper.Get("PUBLISHER", translationFile); + totalSizeTitle.Text = TranslationHelper.Get("TOTAL_SIZE", translationFile); + modDateTitle.Text = TranslationHelper.Get("LAST_MODIFIED", translationFile); + capabilitiesGroupBox.Header = TranslationHelper.Get("CAPABILITIES", translationFile); + } + + public void DisplayInfo(string path) + { + var name = Path.GetFileName(path); + filename.Text = string.IsNullOrEmpty(name) ? path : name; + + _ = Task.Run(() => + { + if (File.Exists(path)) + { + var size = new FileInfo(path).Length; + AppxInfo appxInfo = AppxParser.Parse(path); + var last = File.GetLastWriteTime(path); + + Dispatcher.Invoke(() => + { + productName.Text = appxInfo.ProductName; + productVersion.Text = appxInfo.ProductVersion; + publisher.Text = appxInfo.Publisher; + totalSize.Text = size.ToPrettySize(2); + modDate.Text = last.ToString(CultureInfo.CurrentCulture); + capabilities.ItemsSource = appxInfo.Capabilities; + + using var icon = appxInfo.Logo; + image.Source = icon?.ToBitmapSource() ?? GetWindowsThumbnail(path); + }); + } + }); + + static BitmapSource GetWindowsThumbnail(string path) + { + var scale = DisplayDeviceHelper.GetCurrentScaleFactor(); + using var icon = + WindowsThumbnailProvider.GetThumbnail(path, + (int)(128 * scale.Horizontal), + (int)(128 * scale.Vertical), + ThumbnailOptions.ScaleUp); + var source = icon?.ToBitmapSource(); + + return source; + } + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxBundleReader.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxBundleReader.cs new file mode 100644 index 0000000..95f516c --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxBundleReader.cs @@ -0,0 +1,136 @@ +// Copyright © 2017-2025 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using ICSharpCode.SharpZipLib.Zip; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Xml; + +namespace QuickLook.Plugin.AppViewer.AppxPackageParser; + +public class AppxBundleReader : IDisposable +{ + private ZipFile zip; + private AppxReader appxReader = null; + + private string name; + private string publisher; + private string version; + + public string Name => name ?? appxReader?.Name; + public string Publisher => publisher ?? appxReader?.Publisher; + public string Version => version ?? appxReader?.Version; + public string DisplayName => appxReader?.DisplayName; + public string PublisherDisplayName => appxReader?.PublisherDisplayName; + public string Description => appxReader?.Description; + public string Logo => appxReader?.Logo; + public string[] Capabilities => appxReader?.Capabilities; + public Bitmap Icon => appxReader?.Icon; + + public AppxBundleReader(Stream stream) + { + Open(stream); + } + + public AppxBundleReader(string path) + { + Open(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + + public void Dispose() + { + appxReader?.Dispose(); + appxReader = null; + zip?.Close(); + zip = null; + } + + private void Open(Stream stream) + { + zip = new ZipFile(stream); + ZipEntry entry = zip.GetEntry("AppxMetadata/AppxBundleManifest.xml") + ?? throw new InvalidDataException("AppxMetadata/AppxBundleManifest.xml not found"); + + XmlDocument xml = new() { XmlResolver = null }; + xml.Load(zip.GetInputStream(entry)); + + XmlElement bundleNode = xml.DocumentElement; + + // Identity + { + XmlElement identityNode = bundleNode["Identity"]; + + if (identityNode != null) + { + name = identityNode.Attributes["Name"]?.Value; + version = identityNode.Attributes["Version"]?.Value; + publisher = identityNode.Attributes["Publisher"]?.Value; + + Match m = Regex.Match(Publisher, @"CN=([^,]*),?"); + if (m.Success) publisher = m.Groups[1].Value; + } + } + + // Packages + { + XmlElement packagesNode = bundleNode["Packages"]; + + if (packagesNode != null) + { + string arch = (RuntimeInformation.OSArchitecture == Architecture.Arm64) + ? "arm64" + : (Environment.Is64BitProcess ? "x64" : "x86"); + + var packages = packagesNode.ChildNodes.Select(package => + { + return new + { + Type = package.Attributes["Type"]?.Value, + Architecture = package.Attributes["Architecture"]?.Value, + FileName = package.Attributes["FileName"]?.Value, + Version = package.Attributes["Version"]?.Value + }; + }); + + var package = packages + .Where(p => p.Type == "application" && p.Architecture == arch) + .FirstOrDefault() ?? packages.FirstOrDefault(); + + if (package != null) + { + appxReader = new AppxReader(zip.GetInputStream(zip.GetEntry(package.FileName))); + } + } + } + } +} + +file static class LinqExtension +{ + public static IEnumerable Select(this XmlNodeList nodes, Func selector) + { + foreach (XmlNode node in nodes) + { + yield return selector(node); + } + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxInfo.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxInfo.cs new file mode 100644 index 0000000..f44de9b --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxInfo.cs @@ -0,0 +1,33 @@ +// Copyright © 2017-2025 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.Drawing; + +namespace QuickLook.Plugin.AppViewer.AppxPackageParser; + +public class AppxInfo +{ + public string ProductName { get; set; } + + public string ProductVersion { get; set; } + + public string Publisher { get; set; } + + public Bitmap Logo { get; set; } + + public string[] Capabilities { get; set; } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxParser.cs new file mode 100644 index 0000000..1be7d0d --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxParser.cs @@ -0,0 +1,59 @@ +// Copyright © 2017-2025 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.IO; + +namespace QuickLook.Plugin.AppViewer.AppxPackageParser; + +public static class AppxParser +{ + public static AppxInfo Parse(string path) + { + bool isBundle = Path.GetExtension(path).ToLower() switch + { + ".msixbundle" or ".appxbundle" => true, + _ => false, + }; + + if (isBundle) + { + AppxBundleReader appxReader = new(path); + + return new AppxInfo + { + ProductName = appxReader.DisplayName, + ProductVersion = appxReader.Version, + Publisher = appxReader.Publisher, + Logo = appxReader.Icon, + Capabilities = appxReader.Capabilities, + }; + } + else + { + AppxReader appxReader = new(path); + + return new AppxInfo + { + ProductName = appxReader.DisplayName, + ProductVersion = appxReader.Version, + Publisher = appxReader.Publisher, + Logo = appxReader.Icon, + Capabilities = appxReader.Capabilities, + }; + } + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxReader.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxReader.cs new file mode 100644 index 0000000..cf382e4 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxPackageParser/AppxReader.cs @@ -0,0 +1,155 @@ +// Copyright © 2017-2025 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using ICSharpCode.SharpZipLib.Zip; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Text.RegularExpressions; +using System.Xml; + +namespace QuickLook.Plugin.AppViewer.AppxPackageParser; + +public class AppxReader : IDisposable +{ + private ZipFile zip; + + public string Name { get; set; } + public string Publisher { get; set; } + public string Version { get; set; } + public string DisplayName { get; set; } + public string PublisherDisplayName { get; set; } + public string Description { get; set; } + public string Logo { get; set; } + public string[] Capabilities { get; set; } + + public Bitmap Icon + { + get + { + if (string.IsNullOrEmpty(Logo)) return null; + + string extension = Path.GetExtension(Logo); + string name = Logo.Substring(0, Logo.Length - extension.Length); + + ZipEntry logoEntry = null; + int logoScale = 0; + + foreach (ZipEntry entry in zip) + { + Match m = Regex.Match(entry.Name, @$"{name}(\.scale\-(\d+))?{extension}"); + + if (m.Success) + { + if (int.TryParse(m.Groups[2].Value, out int currentScale)) + { + if (currentScale > logoScale) + { + logoEntry = entry; + logoScale = currentScale; + } + } + } + } + + if (logoEntry != null) + { + return new Bitmap(zip.GetInputStream(logoEntry)); + } + return null; + } + } + + public AppxReader(Stream stream) + { + zip = new ZipFile(stream); + Open(); + } + + public AppxReader(string path) + { + zip = new ZipFile(path); + Open(); + } + + public void Dispose() + { + zip?.Close(); + zip = null; + } + + private void Open() + { + ZipEntry entry = zip.GetEntry("AppxManifest.xml") + ?? throw new InvalidDataException("AppxManifest.xml not found"); + + XmlDocument xml = new() { XmlResolver = null }; + xml.Load(zip.GetInputStream(entry)); + + XmlElement packageNode = xml.DocumentElement; + + // Identity + { + XmlElement identityNode = packageNode["Identity"]; + + if (identityNode != null) + { + Name = identityNode.Attributes["Name"]?.Value; + Version = identityNode.Attributes["Version"]?.Value; + Publisher = identityNode.Attributes["Publisher"]?.Value; + + Match m = Regex.Match(Publisher, @"CN=([^,]*),?"); + if (m.Success) Publisher = m.Groups[1].Value; + } + } + + // Properties + { + XmlElement propertiesNode = packageNode["Properties"]; + + if (propertiesNode != null) + { + DisplayName = propertiesNode["DisplayName"]?.FirstChild?.Value; + PublisherDisplayName = propertiesNode["PublisherDisplayName"]?.FirstChild?.Value; + Description = propertiesNode["Description"]?.FirstChild?.Value; + Logo = propertiesNode["Logo"]?.FirstChild?.Value.Replace(@"\", "/"); + } + } + + // Capabilities + { + XmlElement capabilitiesNode = packageNode["Capabilities"]; + + if (capabilitiesNode != null) + { + Capabilities = [.. capabilitiesNode.ChildNodes.Select(capability => capability.Attributes["Name"]?.Value)]; + } + } + } +} + +file static class LinqExtension +{ + public static IEnumerable Select(this XmlNodeList nodes, Func selector) + { + foreach (XmlNode node in nodes) + { + yield return selector(node); + } + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IAppInfoPanel.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IAppInfoPanel.cs index 3b2f374..f965a31 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IAppInfoPanel.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IAppInfoPanel.cs @@ -22,6 +22,4 @@ public interface IAppInfoPanel public void DisplayInfo(string path); public object Tag { get; set; } - - public bool Stop { get; set; } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiInfoPanel.xaml.cs index 30dd0f4..4a810f5 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiInfoPanel.xaml.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiInfoPanel.xaml.cs @@ -17,7 +17,7 @@ using QuickLook.Common.ExtensionMethods; using QuickLook.Common.Helpers; -using QuickLook.Plugin.AppViewer.MsiImageParser; +using QuickLook.Plugin.AppViewer.MsiPackageParser; using System; using System.Globalization; using System.IO; @@ -29,8 +29,6 @@ namespace QuickLook.Plugin.AppViewer; public partial class MsiInfoPanel : UserControl, IAppInfoPanel { - private bool _stop; - public MsiInfoPanel() { InitializeComponent(); @@ -43,12 +41,6 @@ public partial class MsiInfoPanel : UserControl, IAppInfoPanel modDateTitle.Text = TranslationHelper.Get("LAST_MODIFIED", translationFile); } - public bool Stop - { - set => _stop = value; - get => _stop; - } - public void DisplayInfo(string path) { _ = Task.Run(() => @@ -70,8 +62,6 @@ public partial class MsiInfoPanel : UserControl, IAppInfoPanel var name = Path.GetFileName(path); filename.Text = string.IsNullOrEmpty(name) ? path : name; - Stop = false; - _ = Task.Run(() => { if (File.Exists(path)) diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiImageParser/MsiInfo.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiPackageParser/MsiInfo.cs similarity index 94% rename from QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiImageParser/MsiInfo.cs rename to QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiPackageParser/MsiInfo.cs index 7cf13f4..af9d208 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiImageParser/MsiInfo.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiPackageParser/MsiInfo.cs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -namespace QuickLook.Plugin.AppViewer.MsiImageParser; +namespace QuickLook.Plugin.AppViewer.MsiPackageParser; public sealed class MsiInfo { diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiImageParser/MsiParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiPackageParser/MsiParser.cs similarity index 96% rename from QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiImageParser/MsiParser.cs rename to QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiPackageParser/MsiParser.cs index 991481b..dc22b01 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiImageParser/MsiParser.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiPackageParser/MsiParser.cs @@ -17,7 +17,7 @@ using WixToolset.Dtf.WindowsInstaller; -namespace QuickLook.Plugin.AppViewer.MsiImageParser; +namespace QuickLook.Plugin.AppViewer.MsiPackageParser; public static class MsiParser { diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs index 11b3636..4576ce4 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs @@ -33,9 +33,9 @@ public class Plugin : IViewer //".aab", // Android App Bundle // Windows - //".appx", ".appxbundle", // Windows APPX installer + ".appx", ".appxbundle", // Windows APPX installer ".msi", // Windows MSI installer - //".msix", ".msixbundle", // Windows MSIX installer + ".msix", ".msixbundle", // Windows MSIX installer // macOS //".dmg", // macOS DMG @@ -67,9 +67,10 @@ public class Plugin : IViewer public void Prepare(string path, ContextObject context) { - context.PreferredSize = Path.GetExtension(path) switch + context.PreferredSize = Path.GetExtension(path).ToLower() switch { ".msi" => new Size { Width = 520, Height = 230 }, + ".msix" or ".msixbundle" or ".appx" or ".appxbundle" => new Size { Width = 560, Height = 320 }, _ => throw new NotSupportedException("Extension is not supported."), }; } @@ -77,9 +78,10 @@ public class Plugin : IViewer public void View(string path, ContextObject context) { _path = path; - _ip = Path.GetExtension(path) switch + _ip = Path.GetExtension(path).ToLower() switch { ".msi" => new MsiInfoPanel(), + ".msix" or ".msixbundle" or ".appx" or ".appxbundle" => new AppxInfoPanel(), _ => throw new NotSupportedException("Extension is not supported."), }; diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj index e1ebf3e..8912532 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj @@ -66,8 +66,9 @@ - - + + + diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config index cbd7be4..cebd5d5 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config @@ -7,6 +7,8 @@ Manufacturer Total Size Last Modified + Publisher + Capabilities Versão do produto @@ -14,6 +16,8 @@ Fabricante Tamanho total Modificado em + Editora + Recursos 产品版本 @@ -21,6 +25,8 @@ 制造商 总大小 修改时间 + 发布者 + 功能 產品版本 @@ -28,6 +34,8 @@ 製造商 縂大小 修改時間 + 發行者 + 功能 製品バージョン @@ -35,5 +43,7 @@ 製造元 合計サイズ 更新日時 + 発行元 + 機能