mirror of
https://github.com/QL-Win/QuickLook.git
synced 2025-09-12 10:19:07 +00:00
Support .apk and .apk.1
This commit is contained in:
244
QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml
Normal file
244
QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml
Normal file
@@ -0,0 +1,244 @@
|
||||
<UserControl x:Class="QuickLook.Plugin.AppViewer.ApkInfoPanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:QuickLook.Plugin.AppViewer"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
FontSize="14"
|
||||
UseLayoutRounding="True"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="15" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="150" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image x:Name="image"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Width="120"
|
||||
Height="120"
|
||||
Margin="8"
|
||||
VerticalAlignment="Top"
|
||||
Opacity="0"
|
||||
Stretch="Fill">
|
||||
<Image.Style>
|
||||
<Style TargetType="{x:Type Image}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Source, ElementName=image}" Value="{x:Null}">
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation BeginTime="0:0:0"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0"
|
||||
To="1"
|
||||
Duration="0:0:0.05" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Image.Style>
|
||||
</Image>
|
||||
<Grid Grid.Row="1" Grid.Column="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="10" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="* " />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
MaxHeight="60"
|
||||
Padding="3"
|
||||
FontSize="19"
|
||||
FontWeight="SemiBold"
|
||||
LineHeight="25"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap">
|
||||
<TextBlock.Inlines>
|
||||
<Run x:Name="filename" Text="FilenameFilenameFilenameFilenameFilenameFilenameFilenameFilenameFilenameFilename.ext" />
|
||||
</TextBlock.Inlines>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<!-- Application Name -->
|
||||
<TextBlock x:Name="applicationNameTitle"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Application Name" />
|
||||
<TextBlock x:Name="applicationName"
|
||||
Grid.Row="3"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Searching..."
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<!-- Version Name -->
|
||||
<TextBlock x:Name="versionNameTitle"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Version" />
|
||||
<TextBlock x:Name="versionName"
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Searching..."
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<!-- Version Code -->
|
||||
<TextBlock x:Name="versionCodeTitle"
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Version Code" />
|
||||
<TextBlock x:Name="versionCode"
|
||||
Grid.Row="5"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Searching..."
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<!-- Package Name -->
|
||||
<TextBlock x:Name="packageNameTitle"
|
||||
Grid.Row="6"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Package Name" />
|
||||
<TextBlock x:Name="packageName"
|
||||
Grid.Row="6"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Searching..."
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<!-- MinSdkVersion -->
|
||||
<TextBlock x:Name="minSdkVersionTitle"
|
||||
Grid.Row="7"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="MinSdkVersion" />
|
||||
<TextBlock x:Name="minSdkVersion"
|
||||
Grid.Row="7"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Searching..."
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<!-- TargetSdkVersion -->
|
||||
<TextBlock x:Name="targetSdkVersionTitle"
|
||||
Grid.Row="8"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="TargetSdkVersion" />
|
||||
<TextBlock x:Name="targetSdkVersion"
|
||||
Grid.Row="8"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Searching..."
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<!-- Total Size -->
|
||||
<TextBlock x:Name="totalSizeTitle"
|
||||
Grid.Row="9"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Total Size" />
|
||||
<TextBlock x:Name="totalSize"
|
||||
Grid.Row="9"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Calculating size..." />
|
||||
<!-- Last Modified -->
|
||||
<TextBlock x:Name="modDateTitle"
|
||||
Grid.Row="10"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Last Modified" />
|
||||
<TextBlock x:Name="modDate"
|
||||
Grid.Row="10"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Searching..."
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<!-- Permissions -->
|
||||
<GroupBox x:Name="permissionsGroupBox"
|
||||
Grid.Row="11"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,0,16,16"
|
||||
BorderThickness="0.4"
|
||||
Header="Capabilities">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ScrollViewer.Resources>
|
||||
<Style TargetType="{x:Type TextBox}">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
</Style>
|
||||
</ScrollViewer.Resources>
|
||||
<ItemsControl x:Name="permissions">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Margin="8,3,16,3"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding ., Mode=OneTime}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
|
||||
|
||||
/// <summary>
|
||||
/// https://github.com/hylander0/Iteedee.ApkReader
|
||||
/// </summary>
|
||||
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<string> Permissions { get; set; } = [];
|
||||
|
||||
public List<string> 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<string, List<string>> 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
|
||||
namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
|
||||
|
||||
/// <summary>
|
||||
/// https://github.com/hylander0/Iteedee.ApkReader
|
||||
/// </summary>
|
||||
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<numbStrings; ii++) {
|
||||
// // Length of string starts at StringTable plus offset in StrIndTable
|
||||
// String str = CompXmlString(xml, sitOff, stOff, ii);
|
||||
// tr.add(String.valueOf(ii), str);
|
||||
//}
|
||||
//tr.parent();
|
||||
|
||||
// Step through the XML tree element tags and attributes
|
||||
int off = xmlTagOff;
|
||||
int indent = 0;
|
||||
int startTagLineNo = -2;
|
||||
int startDocTagCounter = 1;
|
||||
while (off < manifestFileData.Length)
|
||||
{
|
||||
int tag0 = LEW(manifestFileData, off);
|
||||
//int tag1 = LEW(manifestFileData, off+1*4);
|
||||
int lineNo = LEW(manifestFileData, off + 2 * 4);
|
||||
//int tag3 = LEW(manifestFileData, off+3*4);
|
||||
int nameNsSi = LEW(manifestFileData, off + 4 * 4);
|
||||
int nameSi = LEW(manifestFileData, off + 5 * 4);
|
||||
|
||||
if (tag0 == startTag)
|
||||
{ // XML START TAG
|
||||
int tag6 = LEW(manifestFileData, off + 6 * 4); // Expected to be 14001400
|
||||
int numbAttrs = LEW(manifestFileData, off + 7 * 4); // Number of Attributes to follow
|
||||
//int tag8 = LEW(manifestFileData, off+8*4); // Expected to be 00000000
|
||||
off += 9 * 4; // Skip over 6+3 words of startTag data
|
||||
string name = CompXmlString(manifestFileData, sitOff, stOff, nameSi);
|
||||
//tr.addSelect(name, null);
|
||||
startTagLineNo = lineNo;
|
||||
|
||||
// Look for the Attributes
|
||||
|
||||
string sb = string.Empty;
|
||||
for (int ii = 0; ii < numbAttrs; ii++)
|
||||
{
|
||||
int attrNameNsSi = LEW(manifestFileData, off); // AttrName Namespace Str Ind, or FFFFFFFF
|
||||
int attrNameSi = LEW(manifestFileData, off + 1 * 4); // AttrName String Index
|
||||
int attrValueSi = LEW(manifestFileData, off + 2 * 4); // AttrValue Str Ind, or FFFFFFFF
|
||||
int attrFlags = LEW(manifestFileData, off + 3 * 4);
|
||||
int attrResId = LEW(manifestFileData, off + 4 * 4); // AttrValue ResourceId or dup AttrValue StrInd
|
||||
off += 5 * 4; // Skip over the 5 words of an attribute
|
||||
|
||||
string attrName = CompXmlString(manifestFileData, sitOff, stOff, attrNameSi);
|
||||
string attrValue = attrValueSi != -1
|
||||
? CompXmlString(manifestFileData, sitOff, stOff, attrValueSi)
|
||||
: /*"resourceID 0x" + */attrResId.ToString();
|
||||
sb += " " + attrName + "=\"" + attrValue + "\"";
|
||||
//tr.add(attrName, attrValue);
|
||||
}
|
||||
PrtIndent(indent, "<" + name + sb + ">");
|
||||
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
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Xml;
|
||||
|
||||
namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
|
||||
|
||||
/// <summary>
|
||||
/// https://github.com/hylander0/Iteedee.ApkReader
|
||||
/// </summary>
|
||||
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<string> 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<string> 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<string> 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<string> 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;
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
|
||||
|
||||
/// <summary>
|
||||
/// https://github.com/hylander0/Iteedee.ApkReader
|
||||
/// </summary>
|
||||
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<string> 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<string, List<string>> responseMap;
|
||||
|
||||
private Dictionary<int, List<string>> entryMap = [];
|
||||
|
||||
public Dictionary<string, List<string>> Initialize()
|
||||
{
|
||||
byte[] data = File.ReadAllBytes("resources.arsc");
|
||||
return ProcessResourceTable(data, []);
|
||||
}
|
||||
|
||||
public Dictionary<string, List<string>> ProcessResourceTable(byte[] data, List<string> 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<string> 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<string, int> 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<string> 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<string> refKs = [.. refKeys.Keys];
|
||||
|
||||
foreach (string refK in refKs)
|
||||
{
|
||||
List<string> 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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -10,9 +10,12 @@
|
||||
<PUBLISHER>Publisher</PUBLISHER>
|
||||
<CAPABILITIES>Capabilities</CAPABILITIES>
|
||||
<APP_NAME>Application</APP_NAME>
|
||||
<APP_VERSION>Version</APP_VERSION>
|
||||
<APP_VERSION_NAME>Version Name</APP_VERSION_NAME>
|
||||
<APP_VERSION_CODE>Version Code</APP_VERSION_CODE>
|
||||
<PERMISSIONS>Permissions</PERMISSIONS>
|
||||
<PACKAGE_NAME>Package Name</PACKAGE_NAME>
|
||||
<APP_MIN_SDK_VERSION>Min SDK Version</APP_MIN_SDK_VERSION>
|
||||
<APP_TARGET_SDK_VERSION>Target SDK Version</APP_TARGET_SDK_VERSION>
|
||||
</en>
|
||||
<pt-BR>
|
||||
<PRODUCT_VERSION>Versão do produto</PRODUCT_VERSION>
|
||||
@@ -23,9 +26,12 @@
|
||||
<PUBLISHER>Editora</PUBLISHER>
|
||||
<CAPABILITIES>Recursos</CAPABILITIES>
|
||||
<APP_NAME>Nome do aplicativo</APP_NAME>
|
||||
<APP_VERSION>Versão</APP_VERSION>
|
||||
<APP_VERSION_NAME>Nome da versão</APP_VERSION_NAME>
|
||||
<APP_VERSION_CODE>Código da versão</APP_VERSION_CODE>
|
||||
<PERMISSIONS>Permissões</PERMISSIONS>
|
||||
<PACKAGE_NAME>Nome do pacote</PACKAGE_NAME>
|
||||
<APP_MIN_SDK_VERSION>Versão mínima do SDK</APP_MIN_SDK_VERSION>
|
||||
<APP_TARGET_SDK_VERSION>Versão alvo do SDK</APP_TARGET_SDK_VERSION>
|
||||
</pt-BR>
|
||||
<zh-CN>
|
||||
<PRODUCT_VERSION>产品版本</PRODUCT_VERSION>
|
||||
@@ -36,9 +42,12 @@
|
||||
<PUBLISHER>发布者</PUBLISHER>
|
||||
<CAPABILITIES>功能</CAPABILITIES>
|
||||
<APP_NAME>应用名称</APP_NAME>
|
||||
<APP_VERSION>版本名称</APP_VERSION>
|
||||
<APP_VERSION_NAME>版本名称</APP_VERSION_NAME>
|
||||
<APP_VERSION_CODE>版本号</APP_VERSION_CODE>
|
||||
<PERMISSIONS>权限</PERMISSIONS>
|
||||
<PACKAGE_NAME>包名</PACKAGE_NAME>
|
||||
<APP_MIN_SDK_VERSION>最低支持 SDK 版本</APP_MIN_SDK_VERSION>
|
||||
<APP_TARGET_SDK_VERSION>目标 SDK 版本</APP_TARGET_SDK_VERSION>
|
||||
</zh-CN>
|
||||
<zh-TW>
|
||||
<PRODUCT_VERSION>產品版本</PRODUCT_VERSION>
|
||||
@@ -49,9 +58,12 @@
|
||||
<PUBLISHER>發行者</PUBLISHER>
|
||||
<CAPABILITIES>功能</CAPABILITIES>
|
||||
<APP_NAME>程式名稱</APP_NAME>
|
||||
<APP_VERSION>版本名稱</APP_VERSION>
|
||||
<APP_VERSION_NAME>版本名稱</APP_VERSION_NAME>
|
||||
<APP_VERSION_CODE>版本號</APP_VERSION_CODE>
|
||||
<PERMISSIONS>權限</PERMISSIONS>
|
||||
<PACKAGE_NAME>套件名稱</PACKAGE_NAME>
|
||||
<APP_MIN_SDK_VERSION>最低支援 SDK 版本</APP_MIN_SDK_VERSION>
|
||||
<APP_TARGET_SDK_VERSION>目標 SDK 版本</APP_TARGET_SDK_VERSION>
|
||||
</zh-TW>
|
||||
<ja>
|
||||
<PRODUCT_VERSION>製品バージョン</PRODUCT_VERSION>
|
||||
@@ -62,8 +74,11 @@
|
||||
<PUBLISHER>発行元</PUBLISHER>
|
||||
<CAPABILITIES>機能</CAPABILITIES>
|
||||
<APP_NAME>アプリケーションネーム</APP_NAME>
|
||||
<APP_VERSION>バージョン</APP_VERSION>
|
||||
<APP_VERSION_NAME>バージョン名</APP_VERSION_NAME>
|
||||
<APP_VERSION_CODE>バージョンコード</APP_VERSION_CODE>
|
||||
<PERMISSIONS>権限</PERMISSIONS>
|
||||
<PACKAGE_NAME>パッケージ名</PACKAGE_NAME>
|
||||
<APP_MIN_SDK_VERSION>最小SDKバージョン</APP_MIN_SDK_VERSION>
|
||||
<APP_TARGET_SDK_VERSION>ターゲットSDKバージョン</APP_TARGET_SDK_VERSION>
|
||||
</ja>
|
||||
</Translations>
|
||||
|
@@ -79,14 +79,14 @@
|
||||
Text="Searching..."
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<!-- Version -->
|
||||
<TextBlock x:Name="versionTitle"
|
||||
<!-- Version Name -->
|
||||
<TextBlock x:Name="versionNameTitle"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Padding="3"
|
||||
Foreground="{DynamicResource WindowTextForegroundAlternative}"
|
||||
Text="Version" />
|
||||
<TextBlock x:Name="version"
|
||||
<TextBlock x:Name="versionName"
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
|
@@ -17,6 +17,7 @@
|
||||
|
||||
using QuickLook.Common.ExtensionMethods;
|
||||
using QuickLook.Common.Helpers;
|
||||
using QuickLook.Common.Plugin;
|
||||
using QuickLook.Plugin.AppViewer.WgtPackageParser;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -28,14 +29,18 @@ namespace QuickLook.Plugin.AppViewer;
|
||||
|
||||
public partial class WgtInfoPanel : UserControl, IAppInfoPanel
|
||||
{
|
||||
public WgtInfoPanel()
|
||||
private ContextObject _context;
|
||||
|
||||
public WgtInfoPanel(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);
|
||||
versionTitle.Text = TranslationHelper.Get("APP_VERSION", translationFile);
|
||||
versionNameTitle.Text = TranslationHelper.Get("APP_VERSION_NAME", translationFile);
|
||||
versionCodeTitle.Text = TranslationHelper.Get("APP_VERSION_CODE", translationFile);
|
||||
totalSizeTitle.Text = TranslationHelper.Get("TOTAL_SIZE", translationFile);
|
||||
modDateTitle.Text = TranslationHelper.Get("LAST_MODIFIED", translationFile);
|
||||
@@ -58,11 +63,13 @@ public partial class WgtInfoPanel : UserControl, IAppInfoPanel
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -15,12 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Json path: version.name
|
||||
/// Json path: versionName.name
|
||||
/// </summary>
|
||||
public string AppVersion { get; set; }
|
||||
public string AppVersionName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Json path: version.code
|
||||
/// Json path: versionName.code
|
||||
/// </summary>
|
||||
public string AppVersionCode { get; set; }
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user