diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapInfoPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapInfoPanel.xaml new file mode 100644 index 0000000..c2e1742 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapInfoPanel.xaml @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapInfoPanel.xaml.cs new file mode 100644 index 0000000..acbccb3 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapInfoPanel.xaml.cs @@ -0,0 +1,132 @@ +// 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.Common.Plugin; +using QuickLook.Plugin.AppViewer.HapPackageParser; +using System; +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 HapInfoPanel : UserControl, IAppInfoPanel +{ + private ContextObject _context; + + public HapInfoPanel(ContextObject context) + { + _context = context; + + DataContext = this; + InitializeComponent(); + + string translationFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations.config"); + applicationNameTitle.Text = TranslationHelper.Get("APP_NAME", translationFile); + versionNameTitle.Text = TranslationHelper.Get("APP_VERSION_NAME", translationFile); + versionCodeTitle.Text = TranslationHelper.Get("APP_VERSION_CODE", translationFile); + bundleNameTitle.Text = TranslationHelper.Get("BUNDLE_NAME", translationFile); + minimumAPIVersionTitle.Text = TranslationHelper.Get("APP_MIN_API_VERSION", translationFile); + targetAPIVersionTitle.Text = TranslationHelper.Get("APP_TARGET_API_VERSION", translationFile); + compileSdkVersionTitle.Text = TranslationHelper.Get("COMPILE_SDK_VERSION", translationFile); + totalSizeTitle.Text = TranslationHelper.Get("TOTAL_SIZE", translationFile); + modDateTitle.Text = TranslationHelper.Get("LAST_MODIFIED", translationFile); + permissionsGroupBox.Header = TranslationHelper.Get("PERMISSIONS", 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; + HapInfo hapInfo = HapParser.Parse(path); + var last = File.GetLastWriteTime(path); + + Dispatcher.Invoke(() => + { + applicationName.Text = hapInfo.Label; + versionName.Text = hapInfo.VersionName; + versionCode.Text = hapInfo.VersionCode; + bundleName.Text = hapInfo.BundleName; + deviceTypes.Text = string.Join(", ", hapInfo.DeviceTypes); + minimumAPIVersion.Text = hapInfo.MinAPIVersion; + targetAPIVersion.Text = hapInfo.TargetAPIVersion; + compileSdkVersion.Text = $"{hapInfo.CompileSdkType} {hapInfo.CompileSdkVersion}"; + totalSize.Text = size.ToPrettySize(2); + modDate.Text = last.ToString(CultureInfo.CurrentCulture); + permissions.ItemsSource = hapInfo.RequestPermissions; + + if (hapInfo.HasIcon) + { + if (hapInfo.Logo?.Length > 0) + { + using var stream = new MemoryStream(hapInfo.Logo); + var icon = new BitmapImage(); + icon.BeginInit(); + icon.CacheOption = BitmapCacheOption.OnLoad; + icon.StreamSource = stream; + icon.EndInit(); + icon.Freeze(); + background.Source = null; + foreground.Source = icon; + } + else if (hapInfo.HasLayeredIcon) + { + { + using var stream = new MemoryStream(hapInfo.AppIconBackground); + var icon = new BitmapImage(); + icon.BeginInit(); + icon.CacheOption = BitmapCacheOption.OnLoad; + icon.StreamSource = stream; + icon.EndInit(); + icon.Freeze(); + background.Source = icon; + } + { + using var stream = new MemoryStream(hapInfo.AppIconForeground); + var icon = new BitmapImage(); + icon.BeginInit(); + icon.CacheOption = BitmapCacheOption.OnLoad; + icon.StreamSource = stream; + icon.EndInit(); + icon.Freeze(); + foreground.Source = icon; + } + } + } + else + { + background.Source = null; + foreground.Source = new BitmapImage(new Uri("pack://application:,,,/QuickLook.Plugin.AppViewer;component/Resources/harmonyos.png")); + } + + _context.IsBusy = false; + }); + } + }); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapPackageParser/HapInfo.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapPackageParser/HapInfo.cs new file mode 100644 index 0000000..b30f1cd --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapPackageParser/HapInfo.cs @@ -0,0 +1,55 @@ +// 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 . + +namespace QuickLook.Plugin.AppViewer.HapPackageParser; + +public sealed class HapInfo +{ + public string Label { get; set; } + + public string Icon { get; set; } + + public byte[] Logo { get; set; } + + public byte[] AppIconForeground { get; set; } + + public byte[] AppIconBackground { get; set; } + + public bool HasIcon { get; set; } = false; + + public bool HasLayeredIcon { get; set; } = false; + + public string VersionName { get; set; } + + public string VersionCode { get; set; } + + public string CompileSdkType { get; set; } + + public string CompileSdkVersion { get; set; } + + public string MinAPIVersion { get; set; } + + public string TargetAPIVersion { get; set; } + + public string BundleName { get; set; } + + public bool Debug { get; set; } + + public string[] RequestPermissions { get; set; } = []; + + public string[] DeviceTypes { get; set; } = []; +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapPackageParser/HapParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapPackageParser/HapParser.cs new file mode 100644 index 0000000..74c0c76 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/HapPackageParser/HapParser.cs @@ -0,0 +1,152 @@ +// 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 Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace QuickLook.Plugin.AppViewer.HapPackageParser; + +internal static class HapParser +{ + public static HapInfo Parse(string path) + { + using var zip = new ZipFile(path); + ZipEntry entry = zip.GetEntry("module.json"); + + if (entry != null) + { + using var stream = zip.GetInputStream(entry); + using var reader = new StreamReader(stream, Encoding.UTF8); + string content = reader.ReadToEnd(); + var dict = JsonConvert.DeserializeObject>(content); + + if (dict == null) return null; + + var info = new HapInfo(); + + if (dict.ContainsKey("app")) + { + dynamic app = dict["app"]; + + if (app.ContainsKey("label")) info.Label = app["label"].ToString(); + if (app.ContainsKey("icon")) info.Icon = app["icon"].ToString(); + if (app.ContainsKey("versionName")) info.VersionName = app["versionName"].ToString(); + if (app.ContainsKey("versionCode")) info.VersionCode = app["versionCode"].ToString(); + if (app.ContainsKey("compileSdkType")) info.CompileSdkType = app["compileSdkType"].ToString(); + if (app.ContainsKey("compileSdkVersion")) info.CompileSdkVersion = app["compileSdkVersion"].ToString(); + if (app.ContainsKey("minAPIVersion")) info.MinAPIVersion = app["minAPIVersion"].ToString(); + if (app.ContainsKey("targetAPIVersion")) info.TargetAPIVersion = app["targetAPIVersion"].ToString(); + if (app.ContainsKey("bundleName")) info.BundleName = app["bundleName"].ToString(); + if (app.ContainsKey("debug")) info.Debug = (bool)app["debug"]; + } + + { + if (info.Icon == "$media:layered_image") + { + ZipEntry foreground = zip.GetEntry("resources/base/media/foreground.png"); + ZipEntry background = zip.GetEntry("resources/base/media/background.png"); + + if (foreground != null && background != null) + { + { + using var s = new BinaryReader(zip.GetInputStream(foreground)); + info.AppIconForeground = s.ReadBytes((int)foreground.Size); + } + { + using var s = new BinaryReader(zip.GetInputStream(background)); + info.AppIconBackground = s.ReadBytes((int)background.Size); + } + + info.HasLayeredIcon = true; + info.HasIcon = true; + } + else + { + info.HasLayeredIcon = false; + info.HasIcon = false; + } + } + + { + ZipEntry appIcon = zip.GetEntry("resources/base/media/app_icon.png"); + + if (appIcon != null) + { + using var s = new BinaryReader(zip.GetInputStream(appIcon)); + info.Logo = s.ReadBytes((int)appIcon.Size); + + info.HasLayeredIcon = false; + info.HasIcon = true; + } + } + + if (!info.HasIcon) + { + ZipEntry icon = zip.GetEntry("resources/base/media/icon.png"); + + if (icon != null) + { + using var s = new BinaryReader(zip.GetInputStream(icon)); + info.Logo = s.ReadBytes((int)icon.Size); + + info.HasLayeredIcon = false; + info.HasIcon = true; + } + } + } + + if (dict.ContainsKey("module")) + { + dynamic module = dict["module"]; + + { + List requestPermissions = []; + + if (module.ContainsKey("requestPermissions")) + { + foreach (dynamic requestPermission in module["requestPermissions"]) + { + if (requestPermission.ContainsKey("name")) + { + requestPermissions.Add(requestPermission["name"].ToString()); + } + } + } + info.RequestPermissions = [.. requestPermissions]; + } + { + List deviceTypes = []; + + if (module.ContainsKey("deviceTypes")) + { + foreach (dynamic deviceType in module["deviceTypes"]) + { + deviceTypes.Add(deviceType.ToString()); + } + } + info.DeviceTypes = [.. deviceTypes]; + } + } + return info; + } + + return null; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs index c551a12..a637c5c 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs @@ -44,7 +44,7 @@ public class Plugin : IViewer ".ipa", // iOS IPA // HarmonyOS - //".hap", // HarmonyOS Package + ".hap", ".hap.1", // HarmonyOS Package //".har", // HarmonyOS Archive // Others @@ -71,6 +71,7 @@ public class Plugin : IViewer { ".apk" => new Size { Width = 560, Height = 500 }, ".ipa" => new Size { Width = 560, Height = 500 }, + ".hap" => new Size { Width = 560, Height = 500 }, ".msi" => new Size { Width = 520, Height = 230 }, ".msix" or ".msixbundle" or ".appx" or ".appxbundle" => new Size { Width = 560, Height = 328 }, ".wgt" or ".wgtu" => new Size { Width = 600, Height = 328 }, @@ -85,6 +86,7 @@ public class Plugin : IViewer { ".apk" => new ApkInfoPanel(context), ".ipa" => new IpaInfoPanel(context), + ".hap" => new HapInfoPanel(context), ".msi" => new MsiInfoPanel(context), ".msix" or ".msixbundle" or ".appx" or ".appxbundle" => new AppxInfoPanel(context), ".wgt" or ".wgtu" => new WgtInfoPanel(context), diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj index b00000d..d0d0b9a 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj @@ -64,12 +64,6 @@ - - - - - - Properties\GitVersion.cs diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Resources/harmonyos.png b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Resources/harmonyos.png new file mode 100644 index 0000000..205ad8b Binary files /dev/null and b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Resources/harmonyos.png differ diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config index 287d2bc..08e8202 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config @@ -19,6 +19,11 @@ Minimum OS Version Target OS Version Device Family + Bundle Name + Device Types + Minimum API Version + Target API Version + Compile SDK Version Versão do produto @@ -38,6 +43,11 @@ Versão mínima do sistema operacional Versão alvo do sistema operacional Família de dispositivos + Nome do pacote + Tipos de dispositivo + Versão mínima do API + Versão mínima do API + Versão do SDK de compilação 产品版本 @@ -57,6 +67,11 @@ 最低系统版本 目标系统版本 设备支持 + 包名 + 设备类型 + 最低支持 API 版本 + 目标 API 版本 + 编译 SDK 版本 產品版本 @@ -76,6 +91,11 @@ 最低作業系統版本 目標作業系統版本 裝置支援 + 套件名稱 + 裝置類型 + 最低支援 API 版本 + 目標 API 版本 + 編譯 SDK 版本 製品バージョン @@ -95,5 +115,10 @@ 最小OSバージョン ターゲットOSバージョン 対応デバイス + バンドル名 + デバイスタイプ + 最小APIバージョン + ターゲットAPIバージョン + コンパイルSDKバージョン diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtParser.cs index 7861593..3c80454 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtParser.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtParser.cs @@ -42,25 +42,25 @@ internal static class WgtParser if (dict == null) return null; - var wgtInfo = new WgtInfo(); + var info = new WgtInfo(); if (dict.ContainsKey("@platforms")) { - wgtInfo.Platforms = ((JArray)dict["@platforms"]).Values().Select(v => v.ToString()).ToArray(); + info.Platforms = ((JArray)dict["@platforms"]).Values().Select(v => v.ToString()).ToArray(); } - if (dict.ContainsKey("id")) wgtInfo.AppId = dict["id"].ToString(); - if (dict.ContainsKey("name")) wgtInfo.AppName = dict["name"].ToString(); + if (dict.ContainsKey("id")) info.AppId = dict["id"].ToString(); + if (dict.ContainsKey("name")) info.AppName = dict["name"].ToString(); if (dict.ContainsKey("version")) { var version = (JObject)dict["version"]; if (version != null) { - if (version.ContainsKey("name")) wgtInfo.AppVersionName = version["name"].ToString(); - if (version.ContainsKey("code")) wgtInfo.AppVersionCode = version["code"].ToString(); + if (version.ContainsKey("name")) info.AppVersionName = version["name"].ToString(); + if (version.ContainsKey("code")) info.AppVersionCode = version["code"].ToString(); } } - if (dict.ContainsKey("description")) wgtInfo.AppDescription = dict["description"].ToString(); + if (dict.ContainsKey("description")) info.AppDescription = dict["description"].ToString(); if (dict.ContainsKey("permissions")) { var permissions = (JObject)dict["permissions"]; @@ -72,7 +72,7 @@ internal static class WgtParser { permissionNames.Add(permission.Key); } - wgtInfo.Permissions = [.. permissionNames]; + info.Permissions = [.. permissionNames]; } } if (dict.ContainsKey("plus")) @@ -92,7 +92,7 @@ internal static class WgtParser { if (kv.Key == "name") { - wgtInfo.AppNameLocales.Add(locale.Key, kv.Value.ToString()); + info.AppNameLocales.Add(locale.Key, kv.Value.ToString()); } } } @@ -103,8 +103,8 @@ internal static class WgtParser var uni_app = plus["uni-app"]; var dictionary = uni_app.ToObject>(); - if (dictionary.ContainsKey("vueVersion")) wgtInfo.VueVersion = dictionary["vueVersion"].ToString(); - if (dictionary.ContainsKey("compilerVersion")) wgtInfo.CompilerVersion = dictionary["compilerVersion"].ToString(); + if (dictionary.ContainsKey("vueVersion")) info.VueVersion = dictionary["vueVersion"].ToString(); + if (dictionary.ContainsKey("compilerVersion")) info.CompilerVersion = dictionary["compilerVersion"].ToString(); } } } @@ -116,13 +116,13 @@ internal static class WgtParser { foreach (var descriptionLocale in descriptionLocales) { - wgtInfo.AppDescriptionLocales.Add(descriptionLocale.Key, descriptionLocale.Value.ToString()); + info.AppDescriptionLocales.Add(descriptionLocale.Key, descriptionLocale.Value.ToString()); } } } - if (dict.ContainsKey("fallbackLocale")) wgtInfo.FallbackLocale = dict["fallbackLocale"].ToString(); + if (dict.ContainsKey("fallbackLocale")) info.FallbackLocale = dict["fallbackLocale"].ToString(); - return wgtInfo; + return info; } return null;