diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml
new file mode 100644
index 0000000..af8ef07
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml.cs
new file mode 100644
index 0000000..84a7c98
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml.cs
@@ -0,0 +1,93 @@
+// 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.ApkPackageParser;
+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 ApkInfoPanel : UserControl, IAppInfoPanel
+{
+ private ContextObject _context;
+
+ public ApkInfoPanel(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);
+ packageNameTitle.Text = TranslationHelper.Get("PACKAGE_NAME", translationFile);
+ minSdkVersionTitle.Text = TranslationHelper.Get("APP_MIN_SDK_VERSION", translationFile);
+ targetSdkVersionTitle.Text = TranslationHelper.Get("APP_TARGET_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;
+ ApkInfo apkInfo = ApkParser.Parse(path);
+ var last = File.GetLastWriteTime(path);
+
+ Dispatcher.Invoke(() =>
+ {
+ applicationName.Text = apkInfo.Label;
+ versionName.Text = apkInfo.VersionName;
+ versionCode.Text = apkInfo.VersionCode;
+ packageName.Text = apkInfo.PackageName;
+ minSdkVersion.Text = apkInfo.MinSdkVersion;
+ targetSdkVersion.Text = apkInfo.TargetSdkVersion;
+ totalSize.Text = size.ToPrettySize(2);
+ modDate.Text = last.ToString(CultureInfo.CurrentCulture);
+ permissions.ItemsSource = apkInfo.Permissions;
+
+ using var stream = new MemoryStream(apkInfo.Logo);
+ var icon = new BitmapImage();
+ icon.BeginInit();
+ icon.CacheOption = BitmapCacheOption.OnLoad;
+ icon.StreamSource = stream;
+ icon.EndInit();
+ icon.Freeze();
+ image.Source = icon;
+
+ _context.IsBusy = false;
+ });
+ }
+ });
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkInfo.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkInfo.cs
new file mode 100644
index 0000000..518cfe0
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkInfo.cs
@@ -0,0 +1,68 @@
+// 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.Collections.Generic;
+
+namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
+
+///
+/// https://github.com/hylander0/Iteedee.ApkReader
+///
+public class ApkInfo
+{
+ public string Label { get; set; }
+
+ public string VersionName { get; set; }
+
+ public string VersionCode { get; set; }
+
+ public string MinSdkVersion { get; set; }
+
+ public string TargetSdkVersion { get; set; }
+
+ public string PackageName { get; set; }
+
+ public string Debuggable { get; set; }
+
+ public List Permissions { get; set; } = [];
+
+ public List IconFileName { get; set; }
+
+ public byte[] Logo { get; set; }
+
+ public bool HasIcon { get; set; } = false;
+
+ public bool SupportSmallScreens { get; set; } = false;
+
+ public bool SupportNormalScreens { get; set; } = false;
+
+ public bool SupportLargeScreens { get; set; } = false;
+
+ public bool SupportAnyDensity { get; set; } = true;
+
+ public Dictionary> ResStrings { get; set; }
+
+ public bool IsDebuggable
+ {
+ get
+ {
+ if (Debuggable == null) return false; // debugabble is not in the manifest
+ if (Debuggable.Equals("-1")) return true; // Debuggable == true
+ else return false;
+ }
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkManifest.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkManifest.cs
new file mode 100644
index 0000000..3b456b2
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkManifest.cs
@@ -0,0 +1,270 @@
+// 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;
+
+namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
+
+///
+/// https://github.com/hylander0/Iteedee.ApkReader
+///
+public class ApkManifest
+{
+ private string result = string.Empty;
+
+ private bool isUtf8;
+
+ // decompressXML -- Parse the 'compressed' binary form of Android XML docs
+ // such as for AndroidManifest.xml in .apk files
+ private const int startDocTag = 0x00100100;
+
+ private const int endDocTag = 0x00100101;
+ private const int startTag = 0x00100102;
+ private const int endTag = 0x00100103;
+ private const int textTag = 0x00100104;
+
+ public string ReadManifestFileIntoXml(byte[] manifestFileData)
+ {
+ if (manifestFileData.Length == 0)
+ throw new Exception("Failed to read manifest data. Byte array was empty");
+ // Compressed XML file/bytes starts with 24x bytes of data,
+ // 9 32 bit words in little endian order (LSB first):
+ // 0th word is 03 00 08 00
+ // 3rd word SEEMS TO BE: Offset at then of StringTable
+ // 4th word is: Number of strings in string table
+ // WARNING: Sometime I indiscriminently display or refer to word in
+ // little endian storage format, or in integer format (ie MSB first).
+ int numbStrings = LEW(manifestFileData, 4 * 4);
+
+ // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
+ // of the length/string data in the StringTable.
+ int sitOff = 0x24; // Offset of start of StringIndexTable
+
+ // StringTable, each string is represented with a 16 bit little endian
+ // character count, followed by that number of 16 bit (LE) (Unicode) chars.
+ int stOff = sitOff + numbStrings * 4; // StringTable follows StrIndexTable
+
+ // XMLTags, The XML tag tree starts after some unknown content after the
+ // StringTable. There is some unknown data after the StringTable, scan
+ // forward from this point to the flag for the start of an XML start tag.
+ int xmlTagOff = LEW(manifestFileData, 3 * 4); // Start from the offset in the 3rd word.
+ // Scan forward until we find the bytes: 0x02011000(x00100102 in normal int)
+
+ // String pool is encoded in UTF-8
+ // https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h#451
+ int flag = LEW(manifestFileData, 4 * 6);
+ this.isUtf8 = (flag & (1 << 8)) > 0;
+
+ for (int ii = xmlTagOff; ii < manifestFileData.Length - 4; ii += 4)
+ {
+ if (LEW(manifestFileData, ii) == startTag)
+ {
+ xmlTagOff = ii; break;
+ }
+ } // end of hack, scanning for start of first start tag
+
+ // XML tags and attributes:
+ // Every XML start and end tag consists of 6 32 bit words:
+ // 0th word: 02011000 for startTag and 03011000 for endTag
+ // 1st word: a flag?, like 38000000
+ // 2nd word: Line of where this tag appeared in the original source file
+ // 3rd word: FFFFFFFF ??
+ // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
+ // 5th word: StringIndex of Element Name
+ // (Note: 01011000 in 0th word means end of XML document, endDocTag)
+
+ // Start tags (not end tags) contain 3 more words:
+ // 6th word: 14001400 meaning??
+ // 7th word: Number of Attributes that follow this tag(follow word 8th)
+ // 8th word: 00000000 meaning??
+
+ // Attributes consist of 5 words:
+ // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
+ // 1st word: StringIndex of Attribute Name
+ // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId used
+ // 3rd word: Flags?
+ // 4th word: str ind of attr value again, or ResourceId of value
+
+ // TMP, dump string table to tr for debugging
+ //tr.addSelect("strings", null);
+ //for (int ii=0; ii");
+ indent++;
+ }
+ else if (tag0 == endTag)
+ {
+ // XML END TAG
+ indent--;
+ off += 6 * 4; // Skip over 6 words of endTag data
+ string name = CompXmlString(manifestFileData, sitOff, stOff, nameSi);
+ PrtIndent(indent, "" + name + "> \r\n"/*+"(line " + startTagLineNo + "-" + lineNo + ")"*/);
+ //tr.parent(); // Step back up the NobTree
+ }
+ else if (tag0 == startDocTag)
+ {
+ startDocTagCounter++;
+ off += 4;
+ }
+ else if (tag0 == endDocTag)
+ {
+ // END OF XML DOC TAG
+ startDocTagCounter--;
+ if (startDocTagCounter == 0)
+ break;
+ }
+ else if (tag0 == textTag)
+ {
+ // code "copied" https://github.com/mikandi/php-apk-parser/blob/fixed-mikandi-versionName/lib/ApkParser/XmlParser.php
+ uint sentinal = 0xffffffff;
+ while (off < manifestFileData.Length)
+ {
+ uint curr = (uint)LEW(manifestFileData, off);
+ off += 4;
+ if (off > manifestFileData.Length)
+ {
+ throw new Exception("Sentinal not found before end of file");
+ }
+ if (curr == sentinal && sentinal == 0xffffffff)
+ {
+ sentinal = 0x00000000;
+ }
+ else if (curr == sentinal)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ Prt(" Unrecognized tag code '" + tag0.ToString("X")
+ + "' at offset " + off);
+ break;
+ }
+ } // end of while loop scanning tags and attributes of XML tree
+ //Prt(" end at offset " + off);
+
+ return result;
+ } // end of decompressXML
+
+ public string CompXmlString(byte[] xml, int sitOff, int stOff, int strInd)
+ {
+ if (strInd < 0) return null;
+ int strOff = stOff + LEW(xml, sitOff + strInd * 4);
+ return CompXmlStringAt(xml, strOff);
+ }
+
+ public static string spaces = " ";
+
+ public void PrtIndent(int indent, string str)
+ {
+ Prt(spaces.Substring(0, Math.Min(indent * 2, spaces.Length)) + str);
+ }
+
+ private void Prt(string p)
+ {
+ result += p;
+ }
+
+ // CompXmlStringAt -- Return the string stored in StringTable format at
+ // offset strOff. This offset points to the 16 bit string length, which
+ // is followed by that number of 16 bit (Unicode) chars.
+ public string CompXmlStringAt(byte[] arr, int strOff)
+ {
+ /**
+ * Strings in UTF-8 format have length indicated by a length encoded in the
+ * stored data. It is either 1 or 2 characters of length data. This allows a
+ * maximum length of 0x7FFF (32767 bytes), but you should consider storing
+ * text in another way if you're using that much data in a single string.
+ *
+ * If the high bit is set, then there are two characters or 2 bytes of length
+ * data encoded. In that case, drop the high bit of the first character and
+ * add it together with the next character.
+ * https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/ResourceTypes.cpp#674
+ */
+ int strLen = arr[strOff];
+ if ((strLen & 0x80) != 0)
+ strLen = ((strLen & 0x7f) << 8) + arr[strOff + 1];
+
+ if (!isUtf8)
+ strLen *= 2;
+
+ byte[] chars = new byte[strLen];
+ for (int ii = 0; ii < strLen; ii++)
+ {
+ chars[ii] = arr[strOff + 2 + ii];
+ }
+
+ return System.Text.Encoding.GetEncoding(isUtf8 ? "UTF-8" : "UTF-16").GetString(chars);
+ } // end of CompXmlStringAt
+
+ // LEW -- Return value of a Little Endian 32 bit word from the byte array
+ // at offset off.
+ public int LEW(byte[] arr, int off)
+ {
+ //return (int)(arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000 | arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF);
+ return (int)(((uint)arr[off + 3]) << 24 & 0xff000000 | ((uint)arr[off + 2]) << 16 & 0xff0000 | ((uint)arr[off + 1]) << 8 & 0xff00 | ((uint)arr[off]) & 0xFF);
+ } // end of LEW
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkParser.cs
new file mode 100644
index 0000000..99e77f6
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkParser.cs
@@ -0,0 +1,60 @@
+// 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.IO;
+using System.Linq;
+
+namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
+
+public static class ApkParser
+{
+ public static ApkInfo Parse(string path)
+ {
+ byte[] manifestData = null;
+ byte[] resourcesData = null;
+
+ using var zip = new ZipFile(path);
+
+ // AndroidManifest.xml
+ {
+ ZipEntry entry = zip.GetEntry("AndroidManifest.xml");
+ using var s = new BinaryReader(zip.GetInputStream(entry));
+ manifestData = s.ReadBytes((int)entry.Size);
+ }
+
+ // resources.arsc
+ {
+ ZipEntry entry = zip.GetEntry("resources.arsc");
+ using var s = new BinaryReader(zip.GetInputStream(entry));
+ resourcesData = s.ReadBytes((int)entry.Size);
+ }
+
+ ApkReader apkReader = new();
+ ApkInfo info = apkReader.ExtractInfo(manifestData, resourcesData);
+
+ // Logo
+ if (info.HasIcon)
+ {
+ ZipEntry entry = zip.GetEntry(info.IconFileName.LastOrDefault());
+ using var s = new BinaryReader(zip.GetInputStream(entry));
+ info.Logo = s.ReadBytes((int)entry.Size);
+ }
+
+ return info;
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkReader.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkReader.cs
new file mode 100644
index 0000000..d658a8a
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkReader.cs
@@ -0,0 +1,280 @@
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Xml;
+
+namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
+
+///
+/// https://github.com/hylander0/Iteedee.ApkReader
+///
+public class ApkReader
+{
+ private const int VER_ID = 0;
+ private const int ICN_ID = 1;
+ private const int LABEL_ID = 2;
+ private readonly string[] VER_ICN = new string[3];
+
+ // Some possible tags and attributes
+ private readonly string[] TAGS = ["manifest", "application", "activity"];
+
+ public string FuzzFindInDocument(XmlDocument doc, string tag, string attr)
+ {
+ foreach (string t in TAGS)
+ {
+ XmlNodeList nodelist = doc.GetElementsByTagName(t);
+ for (int i = 0; i < nodelist.Count; i++)
+ {
+ XmlNode element = nodelist.Item(i);
+ if (element.NodeType == XmlNodeType.Element)
+ {
+ XmlAttributeCollection map = element.Attributes;
+ for (int j = 0; j < map.Count; j++)
+ {
+ XmlNode element2 = map.Item(j);
+ if (element2.Name.EndsWith(attr))
+ {
+ return element2.Value;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private void ExtractPermissions(ApkInfo info, XmlDocument doc)
+ {
+ ExtractPermission(info, doc, "uses-permission", "name");
+ ExtractPermission(info, doc, "permission-group", "name");
+ ExtractPermission(info, doc, "service", "permission");
+ ExtractPermission(info, doc, "provider", "permission");
+ ExtractPermission(info, doc, "activity", "permission");
+ }
+
+ private bool ReadBoolean(XmlDocument doc, string tag, string attribute)
+ {
+ try
+ {
+ string str = FindInDocument(doc, tag, attribute);
+ return Convert.ToBoolean(str);
+ }
+ catch
+ {
+ }
+ return false;
+ }
+
+ private void ExtractSupportScreens(ApkInfo info, XmlDocument doc)
+ {
+ info.SupportSmallScreens = ReadBoolean(doc, "supports-screens", "android:smallScreens");
+ info.SupportNormalScreens = ReadBoolean(doc, "supports-screens", "android:normalScreens");
+ info.SupportLargeScreens = ReadBoolean(doc, "supports-screens", "android:largeScreens");
+
+ if (info.SupportSmallScreens || info.SupportNormalScreens || info.SupportLargeScreens)
+ info.SupportAnyDensity = false;
+ }
+
+ public ApkInfo ExtractInfo(byte[] manifest_xml, byte[] resources_arsx)
+ {
+ string manifestXml;
+ ApkManifest manifest = new();
+ try
+ {
+ manifestXml = manifest.ReadManifestFileIntoXml(manifest_xml);
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+
+ XmlDocument doc = new();
+ doc.LoadXml(manifestXml);
+ return ExtractInfo(doc, resources_arsx);
+ }
+
+ public ApkInfo ExtractInfo(XmlDocument manifestXml, byte[] resources_arsx)
+ {
+ ApkInfo info = new();
+ VER_ICN[VER_ID] = string.Empty;
+ VER_ICN[ICN_ID] = string.Empty;
+ VER_ICN[LABEL_ID] = string.Empty;
+ try
+ {
+ XmlDocument doc = manifestXml ?? throw new Exception("Document initialize failed");
+
+ // Fill up the permission field
+ ExtractPermissions(info, doc);
+
+ // Fill up some basic fields
+ info.MinSdkVersion = FindInDocument(doc, "uses-sdk", "minSdkVersion");
+ info.TargetSdkVersion = FindInDocument(doc, "uses-sdk", "targetSdkVersion");
+ info.VersionCode = FindInDocument(doc, "manifest", "versionCode");
+ info.VersionName = FindInDocument(doc, "manifest", "versionName");
+ info.PackageName = FindInDocument(doc, "manifest", "package");
+
+ info.Label = FindInDocument(doc, "application", "label");
+ if (info.Label.StartsWith("@"))
+ VER_ICN[LABEL_ID] = info.Label;
+ else if (int.TryParse(info.Label, out int labelID))
+ VER_ICN[LABEL_ID] = string.Format("@{0}", labelID.ToString("X4"));
+
+ // Get the value of android:Debuggable in the manifest
+ // "0" = false and "-1" = true
+ info.Debuggable = FindInDocument(doc, "application", "debuggable");
+
+ // Fill up the support screen field
+ ExtractSupportScreens(info, doc);
+
+ info.VersionCode ??= FuzzFindInDocument(doc, "manifest", "versionCode");
+
+ if (info.VersionName == null)
+ info.VersionName = FuzzFindInDocument(doc, "manifest", "versionName");
+ else if (info.VersionName.StartsWith("@"))
+ VER_ICN[VER_ID] = info.VersionName;
+
+ string id = FindInDocument(doc, "application", "android:icon");
+ if (null == id)
+ {
+ id = FuzzFindInDocument(doc, "manifest", "icon");
+ }
+
+ if (null == id)
+ {
+ Debug.WriteLine("icon resId Not Found!");
+ return info;
+ }
+
+ // Find real strings
+ if (!info.HasIcon && id != null)
+ {
+ if (id.StartsWith("@android:"))
+ VER_ICN[ICN_ID] = "@" + id.Substring("@android:".Length);
+ else
+ VER_ICN[ICN_ID] = string.Format("@{0}", Convert.ToInt32(id).ToString("X4"));
+
+ List resId = [];
+
+ for (int i = 0; i < VER_ICN.Length; i++)
+ {
+ if (VER_ICN[i].StartsWith("@"))
+ resId.Add(VER_ICN[i]);
+ }
+
+ ApkResourceFinder finder = new();
+ info.ResStrings = finder.ProcessResourceTable(resources_arsx, resId);
+
+ if (!VER_ICN[VER_ID].Equals(string.Empty))
+ {
+ List versions = null;
+ if (info.ResStrings.ContainsKey(VER_ICN[VER_ID].ToUpper()))
+ versions = info.ResStrings[VER_ICN[VER_ID].ToUpper()];
+ if (versions != null)
+ {
+ if (versions.Count > 0)
+ info.VersionName = versions[0];
+ }
+ else
+ {
+ throw new Exception("VersionName Cant Find in resource with id " + VER_ICN[VER_ID]);
+ }
+ }
+
+ List iconPaths = null;
+ if (info.ResStrings.ContainsKey(VER_ICN[ICN_ID].ToUpper()))
+ iconPaths = info.ResStrings[VER_ICN[ICN_ID].ToUpper()];
+ if (iconPaths != null && iconPaths.Count > 0)
+ {
+ info.IconFileName = [];
+ foreach (string iconFileName in iconPaths)
+ {
+ if (iconFileName != null)
+ {
+ if (iconFileName.Contains(@"/"))
+ {
+ info.IconFileName.Add(iconFileName);
+ info.HasIcon = true;
+ }
+ }
+ }
+ }
+ else
+ {
+ throw new Exception("Icon Cant Find in resource with id " + VER_ICN[ICN_ID]);
+ }
+
+ if (!VER_ICN[LABEL_ID].Equals(string.Empty))
+ {
+ List labels = null;
+ if (info.ResStrings.ContainsKey(VER_ICN[LABEL_ID]))
+ labels = info.ResStrings[VER_ICN[LABEL_ID]];
+ if (labels.Count > 0)
+ {
+ info.Label = labels[0];
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ return info;
+ }
+
+ private void ExtractPermission(ApkInfo info, XmlDocument doc, string keyName, string attribName)
+ {
+ XmlNodeList usesPermissions = doc.GetElementsByTagName(keyName);
+
+ if (usesPermissions != null)
+ {
+ for (int s = 0; s < usesPermissions.Count; s++)
+ {
+ XmlNode permissionNode = usesPermissions.Item(s);
+ if (permissionNode.NodeType == XmlNodeType.Element)
+ {
+ XmlNode node = permissionNode.Attributes.GetNamedItem(attribName);
+ if (node != null)
+ info.Permissions.Add(node.Value);
+ }
+ }
+ }
+ }
+
+ private string FindInDocument(XmlDocument doc, string keyName, string attribName)
+ {
+ XmlNodeList usesPermissions = doc.GetElementsByTagName(keyName);
+
+ if (usesPermissions != null)
+ {
+ for (int s = 0; s < usesPermissions.Count; s++)
+ {
+ XmlNode permissionNode = usesPermissions.Item(s);
+ if (permissionNode.NodeType == XmlNodeType.Element)
+ {
+ XmlNode node = permissionNode.Attributes.GetNamedItem(attribName);
+ if (node != null)
+ return node.Value;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkResourceFinder.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkResourceFinder.cs
new file mode 100644
index 0000000..446de22
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkResourceFinder.cs
@@ -0,0 +1,502 @@
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+
+namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
+
+///
+/// https://github.com/hylander0/Iteedee.ApkReader
+///
+public class ApkResourceFinder
+{
+ private const short RES_STRING_POOL_TYPE = 0x0001;
+ private const short RES_TABLE_TYPE = 0x0002;
+ private const short RES_TABLE_PACKAGE_TYPE = 0x0200;
+ private const short RES_TABLE_TYPE_TYPE = 0x0201;
+ private const short RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
+
+ private string[] valueStringPool = null;
+ private string[] typeStringPool = null;
+ private string[] keyStringPool = null;
+
+ private int package_id = 0;
+ private List resIdList;
+
+ //// Contains no data.
+ //static byte TYPE_NULL = 0x00;
+ //// The 'data' holds an attribute resource identifier.
+ //static byte TYPE_ATTRIBUTE = 0x02;
+ //// The 'data' holds a single-precision floating point number.
+ //static byte TYPE_FLOAT = 0x04;
+ //// The 'data' holds a complex number encoding a dimension value,
+ //// such as "100in".
+ //static byte TYPE_DIMENSION = 0x05;
+ //// The 'data' holds a complex number encoding a fraction of a
+ //// container.
+ //static byte TYPE_FRACTION = 0x06;
+ //// The 'data' is a raw integer value of the form n..n.
+ //static byte TYPE_INT_DEC = 0x10;
+ //// The 'data' is a raw integer value of the form 0xn..n.
+ //static byte TYPE_INT_HEX = 0x11;
+ //// The 'data' is either 0 or 1, for input "false" or "true" respectively.
+ //static byte TYPE_INT_BOOLEAN = 0x12;
+ //// The 'data' is a raw integer value of the form #aarrggbb.
+ //static byte TYPE_INT_COLOR_ARGB8 = 0x1c;
+ //// The 'data' is a raw integer value of the form #rrggbb.
+ //static byte TYPE_INT_COLOR_RGB8 = 0x1d;
+ //// The 'data' is a raw integer value of the form #argb.
+ //static byte TYPE_INT_COLOR_ARGB4 = 0x1e;
+ //// The 'data' is a raw integer value of the form #rgb.
+ //static byte TYPE_INT_COLOR_RGB4 = 0x1f;
+
+ // The 'data' holds a ResTable_ref, a reference to another resource
+ // table entry.
+ private const byte TYPE_REFERENCE = 0x01;
+
+ // The 'data' holds an index into the containing resource table's
+ // global value string pool.
+ private const byte TYPE_STRING = 0x03;
+
+ private Dictionary> responseMap;
+
+ private Dictionary> entryMap = [];
+
+ public Dictionary> Initialize()
+ {
+ byte[] data = File.ReadAllBytes("resources.arsc");
+ return ProcessResourceTable(data, []);
+ }
+
+ public Dictionary> ProcessResourceTable(byte[] data, List resIdList)
+ {
+ this.resIdList = resIdList;
+
+ responseMap = [];
+ long lastPosition;
+
+ using var ms = new MemoryStream(data);
+ using var br = new BinaryReader(ms);
+ short type = br.ReadInt16();
+ short headerSize = br.ReadInt16();
+ int size = br.ReadInt32();
+ int packageCount = br.ReadInt32();
+
+ if (type != RES_TABLE_TYPE)
+ {
+ throw new Exception("No RES_TABLE_TYPE found!");
+ }
+ if (size != br.BaseStream.Length)
+ {
+ throw new Exception("The buffer size not matches to the resource table size.");
+ }
+
+ int realStringPoolCount = 0;
+ int realPackageCount = 0;
+
+ while (true)
+ {
+ long pos = br.BaseStream.Position;
+ short t = br.ReadInt16();
+ short hs = br.ReadInt16();
+ int s = br.ReadInt32();
+
+ if (t == RES_STRING_POOL_TYPE)
+ {
+ if (realStringPoolCount == 0)
+ {
+ // Only the first string pool is processed.
+ Debug.WriteLine("Processing the string pool ...");
+
+ byte[] buffer = new byte[s];
+ lastPosition = br.BaseStream.Position;
+ br.BaseStream.Seek(pos, SeekOrigin.Begin);
+ buffer = br.ReadBytes(s);
+ //br.BaseStream.Seek(lastPosition, SeekOrigin.Begin);
+
+ valueStringPool = ProcessStringPool(buffer);
+ }
+ realStringPoolCount++;
+ }
+ else if (t == RES_TABLE_PACKAGE_TYPE)
+ {
+ // Process the package
+ Debug.WriteLine("Processing package {0} ...", realPackageCount);
+
+ byte[] buffer = new byte[s];
+ lastPosition = br.BaseStream.Position;
+ br.BaseStream.Seek(pos, SeekOrigin.Begin);
+ buffer = br.ReadBytes(s);
+ //br.BaseStream.Seek(lastPosition, SeekOrigin.Begin);
+ ProcessPackage(buffer);
+
+ realPackageCount++;
+ }
+ else
+ {
+ throw new InvalidOperationException("Unsupported Type");
+ }
+ br.BaseStream.Seek(pos + s, SeekOrigin.Begin);
+ if (br.BaseStream.Position == br.BaseStream.Length)
+ break;
+ }
+
+ if (realStringPoolCount != 1)
+ {
+ throw new Exception("More than 1 string pool found!");
+ }
+ if (realPackageCount != packageCount)
+ {
+ throw new Exception("Real package count not equals the declared count.");
+ }
+
+ return responseMap;
+ }
+
+ private void ProcessPackage(byte[] data)
+ {
+ using var ms = new MemoryStream(data);
+ using var br = new BinaryReader(ms);
+ //HEADER
+ short type = br.ReadInt16();
+ short headerSize = br.ReadInt16();
+ int size = br.ReadInt32();
+
+ int id = br.ReadInt32();
+ package_id = id;
+
+ //PackageName
+ char[] name = new char[256];
+ for (int i = 0; i < 256; ++i)
+ {
+ name[i] = br.ReadChar();
+ }
+ int typeStrings = br.ReadInt32();
+ int lastPublicType = br.ReadInt32();
+ int keyStrings = br.ReadInt32();
+ int lastPublicKey = br.ReadInt32();
+
+ if (typeStrings != headerSize)
+ {
+ throw new Exception("TypeStrings must immediately follow the package structure header.");
+ }
+
+ Debug.WriteLine("Type strings:");
+ long lastPosition = br.BaseStream.Position;
+ br.BaseStream.Seek(typeStrings, SeekOrigin.Begin);
+ byte[] bbTypeStrings = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position));
+ br.BaseStream.Seek(lastPosition, SeekOrigin.Begin);
+
+ typeStringPool = ProcessStringPool(bbTypeStrings);
+
+ Debug.WriteLine("Key strings:");
+
+ br.BaseStream.Seek(keyStrings, SeekOrigin.Begin);
+ short key_type = br.ReadInt16();
+ short key_headerSize = br.ReadInt16();
+ int key_size = br.ReadInt32();
+
+ lastPosition = br.BaseStream.Position;
+ br.BaseStream.Seek(keyStrings, SeekOrigin.Begin);
+ byte[] bbKeyStrings = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position));
+ br.BaseStream.Seek(lastPosition, SeekOrigin.Begin);
+
+ keyStringPool = ProcessStringPool(bbKeyStrings);
+
+ // Iterate through all chunks
+ //
+ int typeSpecCount = 0;
+ int typeCount = 0;
+
+ br.BaseStream.Seek((keyStrings + key_size), SeekOrigin.Begin);
+
+ while (true)
+ {
+ int pos = (int)br.BaseStream.Position;
+ short t = br.ReadInt16();
+ short hs = br.ReadInt16();
+ int s = br.ReadInt32();
+
+ if (t == RES_TABLE_TYPE_SPEC_TYPE)
+ {
+ // Process the string pool
+ byte[] buffer = new byte[s];
+ br.BaseStream.Seek(pos, SeekOrigin.Begin);
+ buffer = br.ReadBytes(s);
+
+ ProcessTypeSpec(buffer);
+
+ typeSpecCount++;
+ }
+ else if (t == RES_TABLE_TYPE_TYPE)
+ {
+ // Process the package
+ byte[] buffer = new byte[s];
+ br.BaseStream.Seek(pos, SeekOrigin.Begin);
+ buffer = br.ReadBytes(s);
+
+ ProcessType(buffer);
+
+ typeCount++;
+ }
+
+ br.BaseStream.Seek(pos + s, SeekOrigin.Begin);
+ if (br.BaseStream.Position == br.BaseStream.Length)
+ break;
+ }
+
+ return;
+ }
+
+ private void PutIntoMap(string resId, string value)
+ {
+ List valueList = null;
+ if (responseMap.ContainsKey(resId.ToUpper()))
+ valueList = responseMap[resId.ToUpper()];
+ valueList ??= [];
+ valueList.Add(value);
+ if (responseMap.ContainsKey(resId.ToUpper()))
+ responseMap[resId.ToUpper()] = valueList;
+ else
+ responseMap.Add(resId.ToUpper(), valueList);
+ return;
+ }
+
+ private void ProcessType(byte[] typeData)
+ {
+ using var ms = new MemoryStream(typeData);
+ using var br = new BinaryReader(ms);
+ short type = br.ReadInt16();
+ short headerSize = br.ReadInt16();
+ int size = br.ReadInt32();
+ byte id = br.ReadByte();
+ byte res0 = br.ReadByte();
+ short res1 = br.ReadInt16();
+ int entryCount = br.ReadInt32();
+ int entriesStart = br.ReadInt32();
+
+ Dictionary refKeys = [];
+
+ int config_size = br.ReadInt32();
+
+ // Skip the config data
+ br.BaseStream.Seek(headerSize, SeekOrigin.Begin);
+
+ if (headerSize + entryCount * 4 != entriesStart)
+ {
+ throw new Exception("HeaderSize, entryCount and entriesStart are not valid.");
+ }
+
+ // Start to get entry indices
+ int[] entryIndices = new int[entryCount];
+ for (int i = 0; i < entryCount; ++i)
+ {
+ entryIndices[i] = br.ReadInt32();
+ }
+
+ // Get entries
+ for (int i = 0; i < entryCount; ++i)
+ {
+ if (entryIndices[i] == -1)
+ continue;
+
+ int resource_id = (package_id << 24) | (id << 16) | i;
+
+ long pos = br.BaseStream.Position;
+ short entry_size = br.ReadInt16();
+ short entry_flag = br.ReadInt16();
+ int entry_key = br.ReadInt32();
+
+ // Get the value (simple) or map (complex)
+ int FLAG_COMPLEX = 0x0001;
+
+ if ((entry_flag & FLAG_COMPLEX) == 0)
+ {
+ // Simple case
+ short value_size = br.ReadInt16();
+ byte value_res0 = br.ReadByte();
+ byte value_dataType = br.ReadByte();
+ int value_data = br.ReadInt32();
+
+ string idStr = resource_id.ToString("X4");
+ string keyStr = keyStringPool[entry_key];
+ string data = null;
+
+ Debug.WriteLine("Entry 0x" + idStr + ", key: " + keyStr + ", simple value type: ");
+
+ List entryArr = null;
+ if (entryMap.ContainsKey(int.Parse(idStr, System.Globalization.NumberStyles.HexNumber)))
+ entryArr = entryMap[int.Parse(idStr, System.Globalization.NumberStyles.HexNumber)];
+
+ entryArr ??= [];
+
+ entryArr.Add(keyStr);
+ if (entryMap.ContainsKey(int.Parse(idStr, System.Globalization.NumberStyles.HexNumber)))
+ entryMap[int.Parse(idStr, System.Globalization.NumberStyles.HexNumber)] = entryArr;
+ else
+ entryMap.Add(int.Parse(idStr, System.Globalization.NumberStyles.HexNumber), entryArr);
+
+ if (value_dataType == TYPE_STRING)
+ {
+ data = valueStringPool[value_data];
+ Debug.WriteLine(", data: " + valueStringPool[value_data]);
+ }
+ else if (value_dataType == TYPE_REFERENCE)
+ {
+ string hexIndex = value_data.ToString("X4");
+ refKeys.Add(idStr, value_data);
+ }
+ else
+ {
+ data = value_data.ToString();
+ Debug.WriteLine(", data: " + value_data);
+ }
+
+ // if (inReqList("@" + idStr)) {
+ PutIntoMap("@" + idStr, data);
+ }
+ else
+ {
+ int entry_parent = br.ReadInt32();
+ int entry_count = br.ReadInt32();
+
+ for (int j = 0; j < entry_count; ++j)
+ {
+ int ref_name = br.ReadInt32();
+ short value_size = br.ReadInt16();
+ byte value_res0 = br.ReadByte();
+ byte value_dataType = br.ReadByte();
+ int value_data = br.ReadInt32();
+ }
+
+ Debug.WriteLine("Entry 0x"
+ + resource_id.ToString("X4") + ", key: "
+ + keyStringPool[entry_key]
+ + ", complex value, not printed.");
+ }
+ }
+ HashSet refKs = [.. refKeys.Keys];
+
+ foreach (string refK in refKs)
+ {
+ List values = null;
+ if (responseMap.ContainsKey("@" + refKeys[refK].ToString("X4").ToUpper()))
+ values = responseMap["@" + refKeys[refK].ToString("X4").ToUpper()];
+
+ if (values != null)
+ foreach (string value in values)
+ {
+ PutIntoMap("@" + refK, value);
+ }
+ }
+ return;
+ }
+
+ private string[] ProcessStringPool(byte[] data)
+ {
+ using var ms = new MemoryStream(data);
+ using var br = new BinaryReader(ms);
+ short type = br.ReadInt16();
+ short headerSize = br.ReadInt16();
+ int size = br.ReadInt32();
+ int stringCount = br.ReadInt32();
+ int styleCount = br.ReadInt32();
+ int flags = br.ReadInt32();
+ int stringsStart = br.ReadInt32();
+ int stylesStart = br.ReadInt32();
+
+ bool isUTF_8 = (flags & 256) != 0;
+
+ int[] offsets = new int[stringCount];
+ for (int i = 0; i < stringCount; ++i)
+ {
+ offsets[i] = br.ReadInt32();
+ }
+ string[] strings = new string[stringCount];
+
+ for (int i = 0; i < stringCount; i++)
+ {
+ int pos = stringsStart + offsets[i];
+ br.BaseStream.Seek(pos, SeekOrigin.Begin);
+ strings[i] = string.Empty;
+ if (isUTF_8)
+ {
+ int u16len = br.ReadByte(); // u16len
+ if ((u16len & 0x80) != 0)
+ {
+ // larger than 128
+ u16len = ((u16len & 0x7F) << 8) + br.ReadByte();
+ }
+
+ int u8len = br.ReadByte(); // u8len
+ if ((u8len & 0x80) != 0)
+ {
+ // larger than 128
+ u8len = ((u8len & 0x7F) << 8) + br.ReadByte();
+ }
+
+ if (u8len > 0)
+ strings[i] = Encoding.UTF8.GetString(br.ReadBytes(u8len));
+ else
+ strings[i] = string.Empty;
+ }
+ else // UTF_16
+ {
+ int u16len = br.ReadUInt16();
+ if ((u16len & 0x8000) != 0)
+ {
+ // larger than 32768
+ u16len = ((u16len & 0x7FFF) << 16) + br.ReadUInt16();
+ }
+
+ if (u16len > 0)
+ {
+ strings[i] = Encoding.Unicode.GetString(br.ReadBytes(u16len * 2));
+ }
+ }
+ Debug.WriteLine("Parsed value: {0}", strings[i]);
+ }
+ return strings;
+ }
+
+ private void ProcessTypeSpec(byte[] data)
+ {
+ using var ms = new MemoryStream(data);
+ using var br = new BinaryReader(ms);
+ short type = br.ReadInt16();
+ short headerSize = br.ReadInt16();
+ int size = br.ReadInt32();
+ byte id = br.ReadByte();
+ byte res0 = br.ReadByte();
+ short res1 = br.ReadInt16();
+ int entryCount = br.ReadInt32();
+
+ Debug.WriteLine("Processing type spec {0}", typeStringPool[id - 1]);
+
+ int[] flags = new int[entryCount];
+ for (int i = 0; i < entryCount; ++i)
+ {
+ flags[i] = br.ReadInt32();
+ }
+
+ return;
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml.cs
index d5ab1cc..855f2c1 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/AppxInfoPanel.xaml.cs
@@ -17,6 +17,7 @@
using QuickLook.Common.ExtensionMethods;
using QuickLook.Common.Helpers;
+using QuickLook.Common.Plugin;
using QuickLook.Plugin.AppViewer.AppxPackageParser;
using System.Globalization;
using System.IO;
@@ -29,8 +30,12 @@ namespace QuickLook.Plugin.AppViewer;
public partial class AppxInfoPanel : UserControl, IAppInfoPanel
{
- public AppxInfoPanel()
+ private ContextObject _context;
+
+ public AppxInfoPanel(ContextObject context)
{
+ _context = context;
+
DataContext = this;
InitializeComponent();
@@ -67,6 +72,8 @@ public partial class AppxInfoPanel : UserControl, IAppInfoPanel
using var icon = appxInfo.Logo;
image.Source = icon?.ToBitmapSource() ?? GetWindowsThumbnail(path);
+
+ _context.IsBusy = false;
});
}
});
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiInfoPanel.xaml.cs
index 4a810f5..84bcdc2 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiInfoPanel.xaml.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/MsiInfoPanel.xaml.cs
@@ -17,6 +17,7 @@
using QuickLook.Common.ExtensionMethods;
using QuickLook.Common.Helpers;
+using QuickLook.Common.Plugin;
using QuickLook.Plugin.AppViewer.MsiPackageParser;
using System;
using System.Globalization;
@@ -29,8 +30,12 @@ namespace QuickLook.Plugin.AppViewer;
public partial class MsiInfoPanel : UserControl, IAppInfoPanel
{
- public MsiInfoPanel()
+ private ContextObject _context;
+
+ public MsiInfoPanel(ContextObject context)
{
+ _context = context;
+
InitializeComponent();
string translationFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations.config");
@@ -77,6 +82,8 @@ public partial class MsiInfoPanel : UserControl, IAppInfoPanel
manufacturer.Text = msiInfo.Manufacturer;
totalSize.Text = size.ToPrettySize(2);
modDate.Text = last.ToString(CultureInfo.CurrentCulture);
+
+ _context.IsBusy = false;
});
}
});
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs
index 9ccad0b..4ebec76 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs
@@ -28,7 +28,7 @@ public class Plugin : IViewer
private static readonly string[] _extensions =
[
// Android
- //".apk", ".apk.1", // Android Package
+ ".apk", ".apk.1", // Android Package
//".aar", // Android Archive
//".aab", // Android App Bundle
@@ -67,8 +67,9 @@ public class Plugin : IViewer
public void Prepare(string path, ContextObject context)
{
- context.PreferredSize = Path.GetExtension(path).ToLower() switch
+ context.PreferredSize = Path.GetExtension(ConfirmPath(path)).ToLower() switch
{
+ ".apk" => 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 },
@@ -79,11 +80,12 @@ public class Plugin : IViewer
public void View(string path, ContextObject context)
{
_path = path;
- _ip = Path.GetExtension(path).ToLower() switch
+ _ip = Path.GetExtension(ConfirmPath(path)).ToLower() switch
{
- ".msi" => new MsiInfoPanel(),
- ".msix" or ".msixbundle" or ".appx" or ".appxbundle" => new AppxInfoPanel(),
- ".wgt" or ".wgtu" => new WgtInfoPanel(),
+ ".apk" => new ApkInfoPanel(context),
+ ".msi" => new MsiInfoPanel(context),
+ ".msix" or ".msixbundle" or ".appx" or ".appxbundle" => new AppxInfoPanel(context),
+ ".wgt" or ".wgtu" => new WgtInfoPanel(context),
_ => throw new NotSupportedException("Extension is not supported."),
};
@@ -91,7 +93,6 @@ public class Plugin : IViewer
_ip.Tag = context;
context.ViewerContent = _ip;
- context.IsBusy = false;
}
public void Cleanup()
@@ -100,4 +101,13 @@ public class Plugin : IViewer
_ip = null;
}
+
+ public static string ConfirmPath(string path)
+ {
+ if (Path.GetExtension(path) == ".1")
+ {
+ return path.Substring(0, path.Length - 2);
+ }
+ return path;
+ }
}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config
index 0eab804..ee467f6 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Translations.config
@@ -9,10 +9,13 @@
Last Modified
Publisher
Capabilities
- Application
- Version
- Version Code
- Permissions
+ Application
+ Version Name
+ Version Code
+ Permissions
+ Package Name
+ Min SDK Version
+ Target SDK Version
Versão do produto
@@ -22,10 +25,13 @@
Modificado em
Editora
Recursos
- Nome do aplicativo
- Versão
- Código da versão
- Permissões
+ Nome do aplicativo
+ Nome da versão
+ Código da versão
+ Permissões
+ Nome do pacote
+ Versão mínima do SDK
+ Versão alvo do SDK
产品版本
@@ -35,10 +41,13 @@
修改时间
发布者
功能
- 应用名称
- 版本名称
- 版本号
- 权限
+ 应用名称
+ 版本名称
+ 版本号
+ 权限
+ 包名
+ 最低支持 SDK 版本
+ 目标 SDK 版本
產品版本
@@ -48,10 +57,13 @@
修改時間
發行者
功能
- 程式名稱
- 版本名稱
- 版本號
- 權限
+ 程式名稱
+ 版本名稱
+ 版本號
+ 權限
+ 套件名稱
+ 最低支援 SDK 版本
+ 目標 SDK 版本
製品バージョン
@@ -61,9 +73,12 @@
更新日時
発行元
機能
- アプリケーションネーム
- バージョン
- バージョンコード
- 権限
+ アプリケーションネーム
+ バージョン名
+ バージョンコード
+ 権限
+ パッケージ名
+ 最小SDKバージョン
+ ターゲットSDKバージョン
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtInfoPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtInfoPanel.xaml
index 85a1930..86bbd41 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtInfoPanel.xaml
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtInfoPanel.xaml
@@ -79,14 +79,14 @@
Text="Searching..."
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap" />
-
-
+
-
{
applicationName.Text = wgtInfo.AppNameLocale ?? wgtInfo.AppName;
- version.Text = wgtInfo.AppVersion;
+ versionName.Text = wgtInfo.AppVersionName;
versionCode.Text = wgtInfo.AppVersionCode;
totalSize.Text = size.ToPrettySize(2);
modDate.Text = last.ToString(CultureInfo.CurrentCulture);
permissions.ItemsSource = wgtInfo.Permissions;
+
+ _context.IsBusy = false;
});
}
});
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtInfo.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtInfo.cs
index 9553d15..5caf9af 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtInfo.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtInfo.cs
@@ -15,12 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-using Newtonsoft.Json.Linq;
-using QuickLook.Plugin.AppViewer.AppxPackageParser;
using System.Collections.Generic;
using System.Globalization;
-using System.Resources;
-using System.Text.RegularExpressions;
namespace QuickLook.Plugin.AppViewer.WgtPackageParser;
@@ -84,12 +80,12 @@ public sealed class WgtInfo
public string CompilerVersion { get; set; }
///
- /// Json path: version.name
+ /// Json path: versionName.name
///
- public string AppVersion { get; set; }
+ public string AppVersionName { get; set; }
///
- /// Json path: version.code
+ /// Json path: versionName.code
///
public string AppVersionCode { get; set; }
diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtParser.cs
index 08fb3f5..7861593 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtParser.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/WgtPackageParser/WgtParser.cs
@@ -56,7 +56,7 @@ internal static class WgtParser
if (version != null)
{
- if (version.ContainsKey("name")) wgtInfo.AppVersion = version["name"].ToString();
+ if (version.ContainsKey("name")) wgtInfo.AppVersionName = version["name"].ToString();
if (version.ContainsKey("code")) wgtInfo.AppVersionCode = version["code"].ToString();
}
}