From 00829ec21b5c2028cab85d409e4479eacda371ee Mon Sep 17 00:00:00 2001 From: ema Date: Sat, 31 May 2025 17:30:35 +0800 Subject: [PATCH] Support .apk and .apk.1 --- .../ApkInfoPanel.xaml | 244 +++++++++ .../ApkInfoPanel.xaml.cs | 93 ++++ .../ApkPackageParser/ApkInfo.cs | 68 +++ .../ApkPackageParser/ApkManifest.cs | 270 ++++++++++ .../ApkPackageParser/ApkParser.cs | 60 +++ .../ApkPackageParser/ApkReader.cs | 280 ++++++++++ .../ApkPackageParser/ApkResourceFinder.cs | 502 ++++++++++++++++++ .../AppxInfoPanel.xaml.cs | 9 +- .../MsiInfoPanel.xaml.cs | 9 +- .../QuickLook.Plugin.AppViewer/Plugin.cs | 24 +- .../Translations.config | 55 +- .../WgtInfoPanel.xaml | 6 +- .../WgtInfoPanel.xaml.cs | 13 +- .../WgtPackageParser/WgtInfo.cs | 10 +- .../WgtPackageParser/WgtParser.cs | 2 +- 15 files changed, 1602 insertions(+), 43 deletions(-) create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkInfo.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkManifest.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkParser.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkReader.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkResourceFinder.cs 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, " \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(); } }