diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/NugetInfoPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/NugetInfoPanel.xaml new file mode 100644 index 0000000..ce96051 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/NugetInfoPanel.xaml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/NugetInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/NugetInfoPanel.xaml.cs new file mode 100644 index 0000000..1c16c56 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/NugetInfoPanel.xaml.cs @@ -0,0 +1,138 @@ +// Copyright © 2017-2026 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.Common.Plugin; +using QuickLook.Plugin.AppViewer.PackageParsers.Nuget; +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Navigation; + +namespace QuickLook.Plugin.AppViewer.InfoPanels; + +public partial class NugetInfoPanel : UserControl, IAppInfoPanel +{ + private readonly ContextObject _context; + + public NugetInfoPanel(ContextObject context) + { + _context = context; + + DataContext = this; + InitializeComponent(); + + string translationFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations.config"); + packageIdTitle.Text = TranslationHelper.Get("NUGET_PACKAGE_ID", translationFile); + versionTitle.Text = TranslationHelper.Get("APP_VERSION", translationFile); + authorsTitle.Text = TranslationHelper.Get("NUGET_AUTHORS", translationFile); + licenseTitle.Text = TranslationHelper.Get("NUGET_LICENSE", translationFile); + projectUrlTitle.Text = TranslationHelper.Get("NUGET_PROJECT_URL", translationFile); + repoUrlTitle.Text = TranslationHelper.Get("NUGET_REPO_URL", translationFile); + frameworksTitle.Text = TranslationHelper.Get("NUGET_FRAMEWORKS", translationFile); + totalSizeTitle.Text = TranslationHelper.Get("TOTAL_SIZE", translationFile); + modDateTitle.Text = TranslationHelper.Get("LAST_MODIFIED", translationFile); + dependenciesGroupBox.Header = TranslationHelper.Get("NUGET_DEPENDENCIES", translationFile); + } + + public void DisplayInfo(string path) + { + var name = Path.GetFileName(path); + filename.Text = string.IsNullOrEmpty(name) ? path : name; + + _ = Task.Run(() => + { + if (!File.Exists(path)) return; + + var size = new FileInfo(path).Length; + NugetInfo nugetInfo = NugetParser.Parse(path); + var last = File.GetLastWriteTime(path); + + Dispatcher.Invoke(() => + { + packageId.Text = nugetInfo.PackageId ?? string.Empty; + version.Text = nugetInfo.Version ?? string.Empty; + authors.Text = nugetInfo.Authors ?? string.Empty; + + // License + licenseBlock.Text = nugetInfo.License ?? string.Empty; + + // Project URL + if (!string.IsNullOrWhiteSpace(nugetInfo.ProjectUrl) && + Uri.TryCreate(nugetInfo.ProjectUrl, UriKind.Absolute, out var projectUri)) + { + projectUrlText.Text = nugetInfo.ProjectUrl; + projectUrlLink.NavigateUri = projectUri; + projectUrlTitle.Visibility = Visibility.Visible; + projectUrlBlock.Visibility = Visibility.Visible; + } + + // Source Repository URL + if (!string.IsNullOrWhiteSpace(nugetInfo.RepositoryUrl) && + Uri.TryCreate(nugetInfo.RepositoryUrl, UriKind.Absolute, out var repoUri)) + { + repoUrlText.Text = nugetInfo.RepositoryUrl; + repoUrlLink.NavigateUri = repoUri; + repoUrlTitle.Visibility = Visibility.Visible; + repoUrlBlock.Visibility = Visibility.Visible; + } + + // Target Frameworks + frameworks.Text = nugetInfo.TargetFrameworks?.Length > 0 + ? string.Join(", ", nugetInfo.TargetFrameworks) + : string.Empty; + + totalSize.Text = size.ToPrettySize(2); + modDate.Text = last.ToString(CultureInfo.CurrentCulture); + + // Dependencies + if (nugetInfo.Dependencies?.Length > 0) + { + dependencies.ItemsSource = nugetInfo.Dependencies; + } + + // Icon — use embedded icon if available, otherwise keep default nuget.png + if (nugetInfo.Icon != null) + { + using var icon = nugetInfo.Icon; + image.Source = icon.ToBitmapSource(); + } + + _context.IsBusy = false; + }); + }); + } + + private void OnHyperlinkNavigate(object sender, RequestNavigateEventArgs e) + { + try + { + Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true }); + } + catch + { + // Ignore if the browser cannot be launched + } + e.Handled = true; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Nuget/NugetInfo.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Nuget/NugetInfo.cs new file mode 100644 index 0000000..70daa0f --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Nuget/NugetInfo.cs @@ -0,0 +1,43 @@ +// Copyright © 2017-2026 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.PackageParsers.Nuget; + +public class NugetInfo +{ + public string PackageId { get; set; } + + public string Version { get; set; } + + public string Authors { get; set; } + + public string Description { get; set; } + + public string ProjectUrl { get; set; } + + public string RepositoryUrl { get; set; } + + public string License { get; set; } + + public Bitmap Icon { get; set; } + + public string[] TargetFrameworks { get; set; } = []; + + public string[] Dependencies { get; set; } = []; +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Nuget/NugetParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Nuget/NugetParser.cs new file mode 100644 index 0000000..d22787b --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Nuget/NugetParser.cs @@ -0,0 +1,169 @@ +// Copyright © 2017-2026 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.Xml.Linq; + +namespace QuickLook.Plugin.AppViewer.PackageParsers.Nuget; + +public static class NugetParser +{ + public static NugetInfo Parse(string path) + { + NugetInfo info = new(); + + try + { + using var zip = new ZipFile(path); + + // Find .nuspec entry (only one should exist at the root or in a subdirectory) + ZipEntry nuspecEntry = null; + foreach (ZipEntry entry in zip) + { + if (!entry.IsDirectory && entry.Name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)) + { + nuspecEntry = entry; + break; + } + } + + if (nuspecEntry == null) return info; + + // Parse nuspec XML + XDocument doc; + using (var stream = zip.GetInputStream(nuspecEntry)) + { + doc = XDocument.Load(stream); + } + + // Handle XML namespace (nuspec uses versioned namespace) + XNamespace ns = doc.Root?.GetDefaultNamespace() ?? XNamespace.None; + var metadata = doc.Root?.Element(ns + "metadata"); + if (metadata == null) return info; + + info.PackageId = metadata.Element(ns + "id")?.Value?.Trim(); + info.Version = metadata.Element(ns + "version")?.Value?.Trim(); + info.Authors = metadata.Element(ns + "authors")?.Value?.Trim(); + info.Description = metadata.Element(ns + "description")?.Value?.Trim(); + info.ProjectUrl = metadata.Element(ns + "projectUrl")?.Value?.Trim(); + + // Repository URL + var repoEl = metadata.Element(ns + "repository"); + if (repoEl != null) + { + info.RepositoryUrl = repoEl.Attribute("url")?.Value?.Trim(); + } + + // License: prefer element, fall back to + var licenseEl = metadata.Element(ns + "license"); + if (licenseEl != null) + { + info.License = licenseEl.Value?.Trim(); + } + else + { + info.License = metadata.Element(ns + "licenseUrl")?.Value?.Trim(); + } + + // Icon — look up the embedded icon file in the zip + var iconPath = metadata.Element(ns + "icon")?.Value?.Trim(); + if (!string.IsNullOrEmpty(iconPath)) + { + // Normalize path separators for comparison + string normalizedIconPath = iconPath.Replace('\\', '/'); + foreach (ZipEntry entry in zip) + { + if (!entry.IsDirectory && + string.Equals(entry.Name.Replace('\\', '/'), normalizedIconPath, StringComparison.OrdinalIgnoreCase)) + { + using var iconStream = zip.GetInputStream(entry); + using var ms = new MemoryStream(); + iconStream.CopyTo(ms); + ms.Position = 0; + // Clone the bitmap so it doesn't depend on the stream + using var tmpBitmap = new Bitmap(ms); + info.Icon = new Bitmap(tmpBitmap); + break; + } + } + } + + // Target Frameworks and Dependencies + var depsEl = metadata.Element(ns + "dependencies"); + if (depsEl != null) + { + var groups = depsEl.Elements(ns + "group").ToList(); + + if (groups.Count > 0) + { + // Collect distinct target frameworks + info.TargetFrameworks = groups + .Select(g => g.Attribute("targetFramework")?.Value?.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .Distinct() + .ToArray(); + + // Format all dependencies grouped by framework + var depLines = new List(); + foreach (var group in groups) + { + string fw = group.Attribute("targetFramework")?.Value?.Trim(); + var deps = group.Elements(ns + "dependency").ToList(); + if (deps.Count == 0) continue; + + if (!string.IsNullOrEmpty(fw)) + depLines.Add($"[{fw}]"); + + foreach (var dep in deps) + { + string id = dep.Attribute("id")?.Value?.Trim() ?? string.Empty; + string ver = dep.Attribute("version")?.Value?.Trim() ?? string.Empty; + string exclude = dep.Attribute("exclude")?.Value?.Trim(); + string line = string.IsNullOrEmpty(ver) ? id : $"{id} ({ver})"; + depLines.Add($" {line}"); + } + } + info.Dependencies = depLines.ToArray(); + } + else + { + // No groups — direct top-level dependencies + info.Dependencies = depsEl.Elements(ns + "dependency") + .Select(d => + { + string id = d.Attribute("id")?.Value?.Trim() ?? string.Empty; + string ver = d.Attribute("version")?.Value?.Trim() ?? string.Empty; + return string.IsNullOrEmpty(ver) ? id : $"{id} ({ver})"; + }) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToArray(); + } + } + } + catch + { + // Return whatever partial info was collected + } + + return info; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs index d607f09..487d605 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs @@ -57,6 +57,10 @@ public sealed class Plugin : IViewer // Others ".wgt", ".wgtu", // UniApp Widget + + // NuGet + ".nupkg", // NuGet Package + ".snupkg", // NuGet Symbol Package ]; private IAppInfoPanel _ip; @@ -87,6 +91,7 @@ public sealed class Plugin : IViewer ".appimage" => new Size { Width = 600, Height = 300 }, ".rpm" => new Size { Width = 600, Height = 260 }, ".wgt" or ".wgtu" => new Size { Width = 600, Height = 345 }, + ".nupkg" or ".snupkg" => new Size { Width = 660, Height = 580 }, _ => throw new NotSupportedException("Extension is not supported."), }; context.Title = string.Empty; @@ -112,6 +117,7 @@ public sealed class Plugin : IViewer ".appimage" => new AppImageInfoPanel(context), ".rpm" => new RpmInfoPanel(context), ".wgt" or ".wgtu" => new WgtInfoPanel(context), + ".nupkg" or ".snupkg" => new NugetInfoPanel(context), _ => throw new NotSupportedException("Extension is not supported."), }; diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config index 2ab732b..f3eb31c 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config @@ -33,6 +33,13 @@ النوع طرفية المورد + معرّف الحزمة + المؤلفون + موقع المشروع + مستودع المصدر + الترخيص + أطر العمل المستهدفة + التبعيات Termékverzió @@ -66,6 +73,13 @@ Típus Terminál Szállító + Csomag azonosítója + Szerzők + Projekt webhelye + Forrástár + Licenc + Cél keretrendszerek + Függőségek Verzia produktu @@ -99,6 +113,13 @@ Typ Terminál Dodávateľ + ID balíka + Autori + Webová stránka projektu + Zdrojové úložisko + Licencia + Cieľové rámce + Závislosti Versi produk @@ -132,6 +153,13 @@ Jenis Terminal Vendor + ID Paket + Penulis + Situs Proyek + Repositori Sumber + Lisensi + Target Framework + Dependensi Product Version @@ -165,6 +193,13 @@ Type Terminal Vendor + Package ID + Authors + Project Website + Source Repository + License + Target Frameworks + Dependencies 제품 버전 @@ -198,6 +233,13 @@ 유형 터미널 공급업체 + 패키지 ID + 작성자 + 프로젝트 웹사이트 + 소스 저장소 + 라이선스 + 대상 프레임워크 + 종속성 Versão do produto @@ -231,6 +273,13 @@ Tipo Terminal Fornecedor + ID do pacote + Autores + Website do projeto + Repositório de origem + Licença + Frameworks de destino + Dependências 产品版本 @@ -264,6 +313,13 @@ 类型 终端 供应商 + 包 ID + 作者 + 项目网站 + 源代码仓库 + 许可证 + 目标框架 + 依赖项 產品版本 @@ -297,6 +353,13 @@ 類型 終端機 供應商 + 套件 ID + 作者 + 專案網站 + 原始碼存放庫 + 授權條款 + 目標架構 + 相依性 製品バージョン @@ -330,6 +393,13 @@ 種類 ターミナル ベンダー + パッケージ ID + 作者 + プロジェクトのウェブサイト + ソースリポジトリ + ライセンス + 対応フレームワーク + 依存関係 Produktversion @@ -363,6 +433,13 @@ Typ Terminal Anbieter + Paket-ID + Autoren + Projekt-Website + Quell-Repository + Lizenz + Ziel-Frameworks + Abhängigkeiten Produktversion @@ -396,6 +473,13 @@ Typ Terminal Leverantör + Paket-ID + Upphovsmän + Projektets webbplats + Källkodsförvar + Licens + Målramverk + Beroenden Versiune produs @@ -429,5 +513,12 @@ Tip Terminal Furnizor + ID pachet + Autori + Site-ul proiectului + Depozit sursă + Licență + Framework-uri țintă + Dependențe