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;