feat(gui): new search options to search in text or binary resources (PR #2526)
* search in text or binary resources * load matching tab for scroll to pos in binary panel, treat unknown files as binary in search
This commit is contained in:
@@ -4,31 +4,44 @@ import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import jadx.api.resources.ResourceContentType;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public enum ResourceType {
|
||||
CODE(".dex", ".jar", ".class"),
|
||||
XML(".xml"),
|
||||
ARSC(".arsc"),
|
||||
APK(".apk", ".apkm", ".apks"),
|
||||
FONT(".ttf", ".ttc", ".otf"),
|
||||
IMG(".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
|
||||
ARCHIVE(".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz", ".tgz", ".bz2"),
|
||||
VIDEOS(".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
|
||||
SOUNDS(".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
|
||||
JSON(".json"),
|
||||
TEXT(".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
|
||||
HTML(".html"),
|
||||
LIB(".so"),
|
||||
MANIFEST,
|
||||
UNKNOWN;
|
||||
import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
|
||||
import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
|
||||
import static jadx.api.resources.ResourceContentType.CONTENT_UNKNOWN;
|
||||
|
||||
public enum ResourceType {
|
||||
CODE(CONTENT_BINARY, ".dex", ".jar", ".class"),
|
||||
XML(CONTENT_TEXT, ".xml"),
|
||||
ARSC(CONTENT_TEXT, ".arsc"),
|
||||
APK(CONTENT_BINARY, ".apk", ".apkm", ".apks"),
|
||||
FONT(CONTENT_BINARY, ".ttf", ".ttc", ".otf"),
|
||||
IMG(CONTENT_BINARY, ".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
|
||||
ARCHIVE(CONTENT_BINARY, ".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz",
|
||||
".tgz", ".bz2"),
|
||||
VIDEOS(CONTENT_BINARY, ".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
|
||||
SOUNDS(CONTENT_BINARY, ".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
|
||||
JSON(CONTENT_TEXT, ".json"),
|
||||
TEXT(CONTENT_TEXT, ".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
|
||||
HTML(CONTENT_TEXT, ".html"),
|
||||
LIB(CONTENT_BINARY, ".so"),
|
||||
MANIFEST(CONTENT_TEXT),
|
||||
UNKNOWN_BIN(CONTENT_BINARY, ".bin"),
|
||||
UNKNOWN(CONTENT_UNKNOWN);
|
||||
|
||||
private final ResourceContentType contentType;
|
||||
private final String[] exts;
|
||||
|
||||
ResourceType(String... exts) {
|
||||
ResourceType(ResourceContentType contentType, String... exts) {
|
||||
this.contentType = contentType;
|
||||
this.exts = exts;
|
||||
}
|
||||
|
||||
public ResourceContentType getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public String[] getExts() {
|
||||
return exts;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -229,9 +230,13 @@ public final class ResourcesLoader implements IResourcesLoader {
|
||||
}
|
||||
|
||||
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
|
||||
return loadToCodeWriter(is, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static ICodeInfo loadToCodeWriter(InputStream is, Charset charset) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
|
||||
copyStream(is, baos);
|
||||
return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8));
|
||||
return new SimpleCodeInfo(baos.toString(charset));
|
||||
}
|
||||
|
||||
private synchronized BinaryXMLParser loadBinaryXmlParser() {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package jadx.api.resources;
|
||||
|
||||
public enum ResourceContentType {
|
||||
CONTENT_TEXT,
|
||||
CONTENT_BINARY,
|
||||
CONTENT_NONE,
|
||||
CONTENT_UNKNOWN,
|
||||
}
|
||||
@@ -104,6 +104,14 @@ public class BackgroundExecutor {
|
||||
return execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable)));
|
||||
}
|
||||
|
||||
public void startLoading(Runnable backgroundRunnable, Runnable onFinishUiRunnable) {
|
||||
execute(new SimpleTask(NLS.str("progress.load"), backgroundRunnable, onFinishUiRunnable));
|
||||
}
|
||||
|
||||
public void startLoading(Runnable backgroundRunnable) {
|
||||
execute(new SimpleTask(NLS.str("progress.load"), backgroundRunnable));
|
||||
}
|
||||
|
||||
private synchronized void reset() {
|
||||
taskQueueExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1, Utils.simpleThreadFactory("bg"));
|
||||
taskRunning.clear();
|
||||
|
||||
@@ -8,6 +8,8 @@ import jadx.api.JadxDecompiler;
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.api.JavaPackage;
|
||||
import jadx.core.dex.nodes.PackageNode;
|
||||
import jadx.core.utils.exceptions.InvalidDataException;
|
||||
import jadx.gui.search.providers.ResourceFilter;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
@@ -26,6 +28,7 @@ public class SearchSettings {
|
||||
private Pattern regexPattern;
|
||||
private ISearchMethod searchMethod;
|
||||
private JavaPackage searchPackage;
|
||||
private ResourceFilter resourceFilter;
|
||||
|
||||
public SearchSettings(String searchString) {
|
||||
this.searchString = searchString;
|
||||
@@ -49,6 +52,11 @@ public class SearchSettings {
|
||||
searchPackage = pkg.getJavaNode();
|
||||
}
|
||||
searchMethod = ISearchMethod.build(this);
|
||||
try {
|
||||
resourceFilter = ResourceFilter.parse(resFilterStr);
|
||||
} catch (InvalidDataException e) {
|
||||
return "Invalid resource file filter: " + e.getMessage();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -112,14 +120,14 @@ public class SearchSettings {
|
||||
return searchMethod;
|
||||
}
|
||||
|
||||
public String getResFilterStr() {
|
||||
return resFilterStr;
|
||||
}
|
||||
|
||||
public void setResFilterStr(String resFilterStr) {
|
||||
this.resFilterStr = resFilterStr;
|
||||
}
|
||||
|
||||
public ResourceFilter getResourceFilter() {
|
||||
return resourceFilter;
|
||||
}
|
||||
|
||||
public int getResSizeLimit() {
|
||||
return resSizeLimit;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package jadx.gui.search.providers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import jadx.api.resources.ResourceContentType;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.InvalidDataException;
|
||||
|
||||
import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
|
||||
import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
|
||||
|
||||
public class ResourceFilter {
|
||||
|
||||
private static final ResourceFilter ANY = new ResourceFilter(Set.of(), Set.of());
|
||||
|
||||
private static final String VAR_TEXT = "$TEXT";
|
||||
private static final String VAR_BIN = "$BIN";
|
||||
|
||||
public static final String DEFAULT_STR = VAR_TEXT;
|
||||
|
||||
public static ResourceFilter parse(String filterStr) {
|
||||
String str = filterStr.trim();
|
||||
if (str.isEmpty() || str.equals("*")) {
|
||||
return ANY;
|
||||
}
|
||||
Set<ResourceContentType> contentTypes = EnumSet.noneOf(ResourceContentType.class);
|
||||
Set<String> extSet = new LinkedHashSet<>();
|
||||
String[] parts = filterStr.split("[|, ]");
|
||||
for (String part : parts) {
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (part.startsWith("$")) {
|
||||
switch (part) {
|
||||
case VAR_TEXT:
|
||||
contentTypes.add(CONTENT_TEXT);
|
||||
break;
|
||||
case VAR_BIN:
|
||||
contentTypes.add(CONTENT_BINARY);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException("Unknown var name: " + part);
|
||||
}
|
||||
} else {
|
||||
extSet.add(part);
|
||||
}
|
||||
}
|
||||
return new ResourceFilter(contentTypes, extSet);
|
||||
}
|
||||
|
||||
public static String format(ResourceFilter filter) {
|
||||
if (filter.isAnyFile()) {
|
||||
return "*";
|
||||
}
|
||||
List<String> list = new ArrayList<>();
|
||||
Set<ResourceContentType> types = filter.getContentTypes();
|
||||
if (types.contains(CONTENT_TEXT)) {
|
||||
list.add(VAR_TEXT);
|
||||
}
|
||||
if (types.contains(CONTENT_BINARY)) {
|
||||
list.add(VAR_BIN);
|
||||
}
|
||||
list.addAll(filter.getExtSet());
|
||||
return Utils.listToString(list, "|");
|
||||
}
|
||||
|
||||
public static String withContentType(String filterStr, Set<ResourceContentType> contentTypes) {
|
||||
ResourceFilter filter = parse(filterStr);
|
||||
return format(new ResourceFilter(contentTypes, filter.getExtSet()));
|
||||
}
|
||||
|
||||
private final boolean anyFile;
|
||||
private final Set<ResourceContentType> contentTypes;
|
||||
private final Set<String> extSet;
|
||||
|
||||
private ResourceFilter(Set<ResourceContentType> contentTypes, Set<String> extSet) {
|
||||
this.anyFile = contentTypes.isEmpty() && extSet.isEmpty();
|
||||
this.contentTypes = contentTypes.isEmpty() ? Set.of() : contentTypes;
|
||||
this.extSet = extSet.isEmpty() ? Set.of() : extSet;
|
||||
}
|
||||
|
||||
public boolean isAnyFile() {
|
||||
return anyFile;
|
||||
}
|
||||
|
||||
public Set<ResourceContentType> getContentTypes() {
|
||||
return contentTypes;
|
||||
}
|
||||
|
||||
public Set<String> getExtSet() {
|
||||
return extSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return format(this);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import java.util.ArrayDeque;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.swing.tree.TreeNode;
|
||||
|
||||
@@ -16,6 +14,7 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourceType;
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.api.resources.ResourceContentType;
|
||||
import jadx.api.utils.CodeUtils;
|
||||
import jadx.gui.jobs.Cancelable;
|
||||
import jadx.gui.search.ISearchProvider;
|
||||
@@ -32,10 +31,9 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class);
|
||||
|
||||
private final SearchSettings searchSettings;
|
||||
private final Set<String> extSet;
|
||||
private final SearchDialog searchDialog;
|
||||
private final ResourceFilter resourceFilter;
|
||||
private final int sizeLimit;
|
||||
private boolean anyExt;
|
||||
|
||||
/**
|
||||
* Resources queue for process. Using UI nodes to reuse loading cache
|
||||
@@ -48,7 +46,7 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
|
||||
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings, SearchDialog searchDialog) {
|
||||
this.searchSettings = searchSettings;
|
||||
this.extSet = buildAllowedFilesExtensions(searchSettings.getResFilterStr());
|
||||
this.resourceFilter = searchSettings.getResourceFilter();
|
||||
this.sizeLimit = searchSettings.getResSizeLimit() * 1024 * 1024;
|
||||
this.searchDialog = searchDialog;
|
||||
JResource activeResource = searchSettings.getActiveResource();
|
||||
@@ -95,12 +93,20 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
if (newPos == -1) {
|
||||
return null;
|
||||
}
|
||||
int lineStart = 1 + CodeUtils.getNewLinePosBefore(content, newPos);
|
||||
int lineEnd = CodeUtils.getNewLinePosAfter(content, newPos);
|
||||
int end = lineEnd == -1 ? content.length() : lineEnd;
|
||||
String line = content.substring(lineStart, end);
|
||||
this.pos = end;
|
||||
return new JResSearchNode(resNode, line.trim(), newPos);
|
||||
if (resNode.getContentType() == ResourceContentType.CONTENT_TEXT) {
|
||||
int lineStart = 1 + CodeUtils.getNewLinePosBefore(content, newPos);
|
||||
int lineEnd = CodeUtils.getNewLinePosAfter(content, newPos);
|
||||
int end = lineEnd == -1 ? content.length() : lineEnd;
|
||||
String line = content.substring(lineStart, end);
|
||||
this.pos = end;
|
||||
return new JResSearchNode(resNode, line.trim(), newPos);
|
||||
} else {
|
||||
int start = Math.max(0, newPos - 30);
|
||||
int end = Math.min(newPos + 50, content.length());
|
||||
String line = content.substring(start, end);
|
||||
this.pos = newPos + searchString.length() + 1;
|
||||
return new JResSearchNode(resNode, line, newPos);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable JResource getNextResFile(Cancelable cancelable) {
|
||||
@@ -167,41 +173,41 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
return deque;
|
||||
}
|
||||
|
||||
private Set<String> buildAllowedFilesExtensions(String srhResourceFileExt) {
|
||||
String str = srhResourceFileExt.trim();
|
||||
if (str.isEmpty() || str.equals("*")) {
|
||||
anyExt = true;
|
||||
return Collections.emptySet();
|
||||
private boolean shouldProcess(JResource resNode) {
|
||||
if (resNode.getResFile().getType() == ResourceType.ARSC) {
|
||||
// don't check the size of generated resource table, it will also skip all subfiles
|
||||
return resourceFilter.isAnyFile()
|
||||
|| resourceFilter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT)
|
||||
|| resourceFilter.getExtSet().contains("xml");
|
||||
}
|
||||
Set<String> set = new HashSet<>();
|
||||
for (String extStr : str.split("[|.]")) {
|
||||
String ext = extStr.trim();
|
||||
if (!ext.isEmpty()) {
|
||||
anyExt = ext.equals("*");
|
||||
if (anyExt) {
|
||||
break;
|
||||
}
|
||||
set.add(ext);
|
||||
}
|
||||
if (!isAllowedFileType(resNode)) {
|
||||
return false;
|
||||
}
|
||||
return set;
|
||||
return isAllowedFileSize(resNode);
|
||||
}
|
||||
|
||||
private boolean shouldProcess(JResource resNode) {
|
||||
private boolean isAllowedFileType(JResource resNode) {
|
||||
ResourceFile resFile = resNode.getResFile();
|
||||
if (resFile.getType() == ResourceType.ARSC) {
|
||||
// don't check size of generated resource table, it will also skip all sub files
|
||||
return anyExt || extSet.contains("xml");
|
||||
if (resourceFilter.isAnyFile()) {
|
||||
return true;
|
||||
}
|
||||
if (!anyExt) {
|
||||
String fileExt = CommonFileUtils.getFileExtension(resFile.getOriginalName());
|
||||
if (fileExt == null) {
|
||||
return false;
|
||||
}
|
||||
if (!extSet.contains(fileExt)) {
|
||||
return false;
|
||||
}
|
||||
ResourceContentType resContentType = resNode.getContentType();
|
||||
if (resourceFilter.getContentTypes().contains(resContentType)) {
|
||||
return true;
|
||||
}
|
||||
String fileExt = CommonFileUtils.getFileExtension(resFile.getOriginalName());
|
||||
if (fileExt != null && resourceFilter.getExtSet().contains(fileExt)) {
|
||||
return true;
|
||||
}
|
||||
if (resContentType == ResourceContentType.CONTENT_UNKNOWN
|
||||
&& resourceFilter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY)) {
|
||||
// treat unknown file type as binary
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAllowedFileSize(JResource resNode) {
|
||||
if (sizeLimit <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.util.Objects;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.data.impl.JadxCodeData;
|
||||
import jadx.gui.search.providers.ResourceFilter;
|
||||
|
||||
public class ProjectData {
|
||||
private int projectVersion = 2;
|
||||
@@ -22,7 +23,7 @@ public class ProjectData {
|
||||
private @Nullable String cacheDir; // don't use relative path adapter
|
||||
private boolean enableLiveReload = false;
|
||||
private List<String> searchHistory = new ArrayList<>();
|
||||
private String searchResourcesFilter = "*";
|
||||
private String searchResourcesFilter = ResourceFilter.DEFAULT_STR;
|
||||
private int searchResourcesSizeLimit = 0; // in MB
|
||||
|
||||
protected Map<String, String> pluginOptions = new HashMap<>();
|
||||
|
||||
@@ -17,6 +17,7 @@ import jadx.api.ICodeInfo;
|
||||
import jadx.api.JavaNode;
|
||||
import jadx.api.gui.tree.ITreeNode;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.api.resources.ResourceContentType;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.panel.ContentPanel;
|
||||
@@ -56,6 +57,10 @@ public abstract class JNode extends DefaultMutableTreeNode implements ITreeNode,
|
||||
return ICodeInfo.EMPTY;
|
||||
}
|
||||
|
||||
public ResourceContentType getContentType() {
|
||||
return ResourceContentType.CONTENT_TEXT;
|
||||
}
|
||||
|
||||
public boolean isEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -17,6 +19,7 @@ import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourceType;
|
||||
import jadx.api.ResourcesLoader;
|
||||
import jadx.api.impl.SimpleCodeInfo;
|
||||
import jadx.api.resources.ResourceContentType;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
@@ -157,16 +160,17 @@ public class JResource extends JLoadableNode {
|
||||
switch (resFile.getType()) {
|
||||
case IMG:
|
||||
return new ImagePanel(tabbedPane, this);
|
||||
case LIB:
|
||||
case CODE:
|
||||
return new BinaryContentPanel(tabbedPane, this, false);
|
||||
case FONT:
|
||||
return new FontPanel(tabbedPane, this);
|
||||
}
|
||||
if (getSyntaxByExtension(resFile.getDeobfName()) == null) {
|
||||
return new BinaryContentPanel(tabbedPane, this);
|
||||
if (getContentType() == ResourceContentType.CONTENT_BINARY) {
|
||||
return new BinaryContentPanel(tabbedPane, this, false);
|
||||
}
|
||||
return new CodeContentPanel(tabbedPane, this);
|
||||
if (getSyntaxByExtension(resFile.getDeobfName()) != null) {
|
||||
return new CodeContentPanel(tabbedPane, this);
|
||||
}
|
||||
// unknown file type, show both text and binary
|
||||
return new BinaryContentPanel(tabbedPane, this, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -185,13 +189,18 @@ public class JResource extends JLoadableNode {
|
||||
return codeInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceContentType getContentType() {
|
||||
if (type == JResType.FILE) {
|
||||
return resFile.getType().getContentType();
|
||||
}
|
||||
return ResourceContentType.CONTENT_NONE;
|
||||
}
|
||||
|
||||
private ICodeInfo loadContent() {
|
||||
if (resFile == null || type != JResType.FILE) {
|
||||
return ICodeInfo.EMPTY;
|
||||
}
|
||||
if (!isSupportedForView(resFile.getType())) {
|
||||
return ICodeInfo.EMPTY;
|
||||
}
|
||||
ResContainer rc = resFile.loadContent();
|
||||
if (rc == null) {
|
||||
return ICodeInfo.EMPTY;
|
||||
@@ -216,11 +225,20 @@ public class JResource extends JLoadableNode {
|
||||
|
||||
case RES_LINK:
|
||||
try {
|
||||
return ResourcesLoader.decodeStream(rc.getResLink(), (size, is) -> {
|
||||
ResourceFile resourceFile = rc.getResLink();
|
||||
return ResourcesLoader.decodeStream(resourceFile, (size, is) -> {
|
||||
// TODO: check size before loading
|
||||
if (size > 10 * 1024 * 1024L) {
|
||||
return new SimpleCodeInfo("File too large for view");
|
||||
}
|
||||
return ResourcesLoader.loadToCodeWriter(is);
|
||||
Charset charset;
|
||||
if (resourceFile.getType().getContentType() == ResourceContentType.CONTENT_TEXT) {
|
||||
charset = StandardCharsets.UTF_8;
|
||||
} else {
|
||||
// force one byte charset for binary data to have the same offsets as in a byte array
|
||||
charset = StandardCharsets.US_ASCII;
|
||||
}
|
||||
return ResourcesLoader.loadToCodeWriter(is, charset);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return new SimpleCodeInfo("Failed to load resource file:\n" + Utils.getStackTrace(e));
|
||||
|
||||
@@ -990,13 +990,15 @@ public class MainWindow extends JFrame {
|
||||
ContentPanel panel = tabbedPane.getSelectedContentPanel();
|
||||
if (panel instanceof AbstractCodeContentPanel) {
|
||||
AbstractCodeArea codeArea = ((AbstractCodeContentPanel) panel).getCodeArea();
|
||||
String preferText = codeArea.getSelectedText();
|
||||
if (StringUtils.isEmpty(preferText)) {
|
||||
preferText = codeArea.getWordUnderCaret();
|
||||
}
|
||||
if (!StringUtils.isEmpty(preferText)) {
|
||||
SearchDialog.searchText(MainWindow.this, preferText);
|
||||
return;
|
||||
if (codeArea != null) {
|
||||
String preferText = codeArea.getSelectedText();
|
||||
if (StringUtils.isEmpty(preferText)) {
|
||||
preferText = codeArea.getWordUnderCaret();
|
||||
}
|
||||
if (!StringUtils.isEmpty(preferText)) {
|
||||
SearchDialog.searchText(MainWindow.this, preferText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.TEXT);
|
||||
|
||||
@@ -2,6 +2,8 @@ package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.Component;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.panel.ContentPanel;
|
||||
import jadx.gui.ui.tab.TabbedPane;
|
||||
@@ -16,7 +18,15 @@ public abstract class AbstractCodeContentPanel extends ContentPanel {
|
||||
super(panel, jnode);
|
||||
}
|
||||
|
||||
public abstract AbstractCodeArea getCodeArea();
|
||||
public abstract @Nullable AbstractCodeArea getCodeArea();
|
||||
|
||||
public abstract Component getChildrenComponent();
|
||||
|
||||
public void scrollToPos(int pos) {
|
||||
AbstractCodeArea codeArea = getCodeArea();
|
||||
if (codeArea != null) {
|
||||
codeArea.requestFocus();
|
||||
codeArea.scrollToPos(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package jadx.gui.ui.codearea;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.JTabbedPane;
|
||||
@@ -14,14 +13,15 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ResourcesLoader;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
import jadx.api.resources.ResourceContentType;
|
||||
import jadx.gui.jobs.BackgroundExecutor;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.settings.LineNumbersMode;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
import jadx.gui.ui.hexviewer.HexPreviewPanel;
|
||||
import jadx.gui.ui.tab.TabbedPane;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
public class BinaryContentPanel extends AbstractCodeContentPanel {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BinaryContentPanel.class);
|
||||
@@ -29,10 +29,6 @@ public class BinaryContentPanel extends AbstractCodeContentPanel {
|
||||
private final transient HexPreviewPanel hexPreviewPanel;
|
||||
private final transient JTabbedPane areaTabbedPane;
|
||||
|
||||
public BinaryContentPanel(TabbedPane panel, JNode jnode) {
|
||||
this(panel, jnode, true);
|
||||
}
|
||||
|
||||
public BinaryContentPanel(TabbedPane panel, JNode jnode, boolean supportsText) {
|
||||
super(panel, jnode);
|
||||
setLayout(new BorderLayout());
|
||||
@@ -51,33 +47,26 @@ public class BinaryContentPanel extends AbstractCodeContentPanel {
|
||||
SwingUtilities.invokeLater(this::loadSelectedPanel);
|
||||
}
|
||||
|
||||
private void loadToHexView(JNode binaryNode) {
|
||||
private void loadHexView() {
|
||||
if (hexPreviewPanel.isDataLoaded()) {
|
||||
return;
|
||||
}
|
||||
AtomicReference<byte[]> bytesRef = new AtomicReference<>();
|
||||
getMainWindow().getBackgroundExecutor().execute(NLS.str("progress.load"),
|
||||
() -> {
|
||||
byte[] bytes = null;
|
||||
if (binaryNode instanceof JResource) {
|
||||
JResource jResource = (JResource) binaryNode;
|
||||
try {
|
||||
bytes = ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes());
|
||||
} catch (JadxException e) {
|
||||
LOG.error("Failed to directly load resource binary data {}: {}", jResource.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
if (bytes == null) {
|
||||
bytes = binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
bytesRef.set(bytes);
|
||||
},
|
||||
taskStatus -> {
|
||||
byte[] bytes = bytesRef.get();
|
||||
if (bytes != null) {
|
||||
hexPreviewPanel.setData(bytes);
|
||||
}
|
||||
});
|
||||
UiUtils.notUiThreadGuard();
|
||||
byte[] bytes = getNodeBytes();
|
||||
UiUtils.uiRunAndWait(() -> hexPreviewPanel.setData(bytes));
|
||||
}
|
||||
|
||||
private byte[] getNodeBytes() {
|
||||
JNode binaryNode = getNode();
|
||||
if (binaryNode instanceof JResource) {
|
||||
JResource jResource = (JResource) binaryNode;
|
||||
try {
|
||||
return ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to directly load resource binary data {}: {}", jResource.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
return binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private JTabbedPane buildTabbedPane() {
|
||||
@@ -96,12 +85,13 @@ public class BinaryContentPanel extends AbstractCodeContentPanel {
|
||||
}
|
||||
|
||||
private void loadSelectedPanel() {
|
||||
BackgroundExecutor bgExec = getMainWindow().getBackgroundExecutor();
|
||||
Component codePanel = getSelectedPanel();
|
||||
if (codePanel instanceof CodeArea) {
|
||||
CodeArea codeArea = (CodeArea) codePanel;
|
||||
codeArea.load();
|
||||
bgExec.startLoading(codeArea::load);
|
||||
} else {
|
||||
loadToHexView(getNode());
|
||||
bgExec.startLoading(this::loadHexView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +104,19 @@ public class BinaryContentPanel extends AbstractCodeContentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToPos(int pos) {
|
||||
BackgroundExecutor bgExec = getMainWindow().getBackgroundExecutor();
|
||||
if (getNode().getContentType() == ResourceContentType.CONTENT_TEXT) {
|
||||
areaTabbedPane.setSelectedComponent(textCodePanel);
|
||||
AbstractCodeArea codeArea = textCodePanel.getCodeArea();
|
||||
bgExec.startLoading(codeArea::load, () -> codeArea.scrollToPos(pos));
|
||||
} else {
|
||||
areaTabbedPane.setSelectedComponent(hexPreviewPanel);
|
||||
bgExec.startLoading(this::loadHexView, () -> hexPreviewPanel.scrollToOffset(pos));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getChildrenComponent() {
|
||||
return getSelectedPanel();
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.awt.FlowLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
@@ -51,6 +52,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.api.JavaPackage;
|
||||
import jadx.api.resources.ResourceContentType;
|
||||
import jadx.core.dex.nodes.PackageNode;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.StringUtils;
|
||||
@@ -65,6 +67,7 @@ import jadx.gui.search.providers.CommentSearchProvider;
|
||||
import jadx.gui.search.providers.FieldSearchProvider;
|
||||
import jadx.gui.search.providers.MergedSearchProvider;
|
||||
import jadx.gui.search.providers.MethodSearchProvider;
|
||||
import jadx.gui.search.providers.ResourceFilter;
|
||||
import jadx.gui.search.providers.ResourceSearchProvider;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
@@ -80,6 +83,7 @@ import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.cache.ValueCache;
|
||||
import jadx.gui.utils.layout.WrapLayout;
|
||||
import jadx.gui.utils.rx.RxUtils;
|
||||
import jadx.gui.utils.ui.DocumentUpdateListener;
|
||||
|
||||
import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.ACTIVE_TAB;
|
||||
import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CLASS;
|
||||
@@ -318,15 +322,56 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
searchPackagePanel
|
||||
.setPreferredSize(new Dimension(Math.max(packageField.getPreferredSize().width, minPanelSize.width), minPanelSize.height));
|
||||
|
||||
resExtField = new JTextField();
|
||||
resExtField = new JTextField(30);
|
||||
TextStandardActions.attach(resExtField);
|
||||
resExtField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
|
||||
resExtField.setToolTipText(NLS.str("preferences.res_file_ext"));
|
||||
resExtField.setText(mainWindow.getProject().getSearchResourcesFilter());
|
||||
String resFilterStr = mainWindow.getProject().getSearchResourcesFilter();
|
||||
resExtField.setText(resFilterStr);
|
||||
|
||||
JPanel resExtFilePanel = new JPanel(new BorderLayout());
|
||||
ResourceFilter resFilter = ResourceFilter.parse(resFilterStr);
|
||||
|
||||
JCheckBox textResBox = new JCheckBox(NLS.str("search_dialog.res_text"));
|
||||
textResBox.setSelected(resFilter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT));
|
||||
JCheckBox binResBox = new JCheckBox(NLS.str("search_dialog.res_binary"));
|
||||
binResBox.setSelected(resFilter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY));
|
||||
|
||||
ItemListener resContentTypeListener = ev -> {
|
||||
try {
|
||||
Set<ResourceContentType> contentTypes = EnumSet.noneOf(ResourceContentType.class);
|
||||
if (textResBox.isSelected()) {
|
||||
contentTypes.add(ResourceContentType.CONTENT_TEXT);
|
||||
}
|
||||
if (binResBox.isSelected()) {
|
||||
contentTypes.add(ResourceContentType.CONTENT_BINARY);
|
||||
}
|
||||
String newStr = ResourceFilter.withContentType(resExtField.getText(), contentTypes);
|
||||
if (!newStr.equals(resExtField.getText())) {
|
||||
resExtField.setText(newStr);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
textResBox.addItemListener(resContentTypeListener);
|
||||
binResBox.addItemListener(resContentTypeListener);
|
||||
|
||||
resExtField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> UiUtils.uiRun(() -> {
|
||||
try {
|
||||
ResourceFilter filter = ResourceFilter.parse(resExtField.getText());
|
||||
textResBox.setSelected(filter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT));
|
||||
binResBox.setSelected(filter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY));
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
})));
|
||||
|
||||
JPanel resExtFilePanel = new JPanel();
|
||||
resExtFilePanel.setLayout(new BoxLayout(resExtFilePanel, BoxLayout.LINE_AXIS));
|
||||
resExtFilePanel.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.res_file_ext")));
|
||||
resExtFilePanel.add(resExtField, BorderLayout.CENTER);
|
||||
resExtFilePanel.add(resExtField);
|
||||
resExtFilePanel.add(textResBox);
|
||||
resExtFilePanel.add(binResBox);
|
||||
resExtFilePanel.setPreferredSize(calcMinSizeForTitledBorder(resExtFilePanel));
|
||||
|
||||
resSizeLimit = new JSpinner(new SpinnerNumberModel(mainWindow.getProject().getSearchResourcesSizeLimit(), 0, Integer.MAX_VALUE, 1));
|
||||
@@ -562,10 +607,11 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
|
||||
SearchTask newSearchTask = new SearchTask(mainWindow, this::addSearchResult, this::searchFinished);
|
||||
if (!buildSearch(newSearchTask, text, searchSettings)) {
|
||||
UiUtils.highlightAsErrorField(searchField, true);
|
||||
return null;
|
||||
}
|
||||
// save search settings
|
||||
mainWindow.getProject().setSearchResourcesFilter(searchSettings.getResFilterStr());
|
||||
mainWindow.getProject().setSearchResourcesFilter(resExtField.getText().trim());
|
||||
mainWindow.getProject().setSearchResourcesSizeLimit(searchSettings.getResSizeLimit());
|
||||
return newSearchTask;
|
||||
}
|
||||
|
||||
@@ -135,6 +135,12 @@ public class HexPreviewPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
public void scrollToOffset(int pos) {
|
||||
hexCodeArea.setSelection(pos, pos + 1);
|
||||
hexCodeArea.setActiveCaretPosition(pos);
|
||||
hexCodeArea.centerOnPosition(hexCodeArea.getActiveCaretPosition());
|
||||
}
|
||||
|
||||
public void enableUpdate() {
|
||||
CodeAreaCaretListener caretMovedListener = (CodeAreaCaretPosition caretPosition) -> updateValues();
|
||||
hexCodeArea.addCaretMovedListener(caretMovedListener);
|
||||
|
||||
@@ -3,6 +3,8 @@ package jadx.gui.ui.panel;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
@@ -12,7 +14,7 @@ import jadx.gui.ui.tab.TabbedPane;
|
||||
import jadx.gui.ui.tab.TabsController;
|
||||
|
||||
public abstract class ContentPanel extends JPanel {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ContentPanel.class);
|
||||
private static final long serialVersionUID = 3237031760631677822L;
|
||||
|
||||
protected TabbedPane tabbedPane;
|
||||
@@ -41,6 +43,10 @@ public abstract class ContentPanel extends JPanel {
|
||||
return node;
|
||||
}
|
||||
|
||||
public void scrollToPos(int pos) {
|
||||
LOG.warn("ContentPanel.scrollToPos method not implemented, class: {}", getClass().getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to show a tool tip on the tab e.g. for displaying a long path of the
|
||||
* selected entry inside the APK file.
|
||||
|
||||
@@ -225,26 +225,21 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
|
||||
}
|
||||
|
||||
private @Nullable ContentPanel showCode(JumpPosition jumpPos) {
|
||||
ContentPanel contentPanel = getContentPanel(jumpPos.getNode());
|
||||
if (contentPanel != null) {
|
||||
selectTab(contentPanel);
|
||||
scrollToPos(contentPanel, jumpPos.getPos());
|
||||
JNode jumpNode = jumpPos.getNode();
|
||||
ContentPanel contentPanel = getContentPanel(jumpNode);
|
||||
if (contentPanel == null) {
|
||||
return null;
|
||||
}
|
||||
selectTab(contentPanel);
|
||||
int pos = jumpPos.getPos();
|
||||
if (pos < 0) {
|
||||
LOG.warn("Invalid jump: {}", jumpPos, new JadxRuntimeException());
|
||||
pos = 0;
|
||||
}
|
||||
contentPanel.scrollToPos(pos);
|
||||
return contentPanel;
|
||||
}
|
||||
|
||||
private void scrollToPos(ContentPanel contentPanel, int pos) {
|
||||
if (pos == 0) {
|
||||
LOG.warn("Ignore zero jump!", new JadxRuntimeException());
|
||||
return;
|
||||
}
|
||||
if (contentPanel instanceof AbstractCodeContentPanel) {
|
||||
AbstractCodeArea codeArea = ((AbstractCodeContentPanel) contentPanel).getCodeArea();
|
||||
codeArea.requestFocus();
|
||||
codeArea.scrollToPos(pos);
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTab(ContentPanel contentPanel) {
|
||||
controller.selectTab(contentPanel.getNode());
|
||||
}
|
||||
@@ -259,7 +254,7 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
|
||||
} else {
|
||||
selectTab(panel);
|
||||
}
|
||||
ClassCodeContentPanel codePane = ((ClassCodeContentPanel) panel);
|
||||
ClassCodeContentPanel codePane = (ClassCodeContentPanel) panel;
|
||||
codePane.showSmaliPane();
|
||||
SmaliArea smaliArea = (SmaliArea) codePane.getSmaliCodeArea();
|
||||
if (debugMode) {
|
||||
@@ -272,7 +267,10 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
|
||||
public @Nullable JumpPosition getCurrentPosition() {
|
||||
ContentPanel selectedCodePanel = getSelectedContentPanel();
|
||||
if (selectedCodePanel instanceof AbstractCodeContentPanel) {
|
||||
return ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea().getCurrentPosition();
|
||||
AbstractCodeArea codeArea = ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea();
|
||||
if (codeArea != null) {
|
||||
return codeArea.getCurrentPosition();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.resource=Ressource
|
||||
search_dialog.keep_open=Offen halten
|
||||
search_dialog.tip_searching=Suchen…
|
||||
search_dialog.limit_package=Begrenzung auf Paket:
|
||||
#search_dialog.res_text=Text
|
||||
#search_dialog.res_binary=Binary
|
||||
search_dialog.package_not_found=Kein passendes Paket gefunden
|
||||
search_dialog.copy=alles kopieren
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.resource=Resource
|
||||
search_dialog.keep_open=Keep open
|
||||
search_dialog.tip_searching=Searching
|
||||
search_dialog.limit_package=Limit to package:
|
||||
search_dialog.res_text=Text
|
||||
search_dialog.res_binary=Binary
|
||||
search_dialog.package_not_found=No matching package found
|
||||
search_dialog.copy=Copy All
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.regex=Regex
|
||||
#search_dialog.keep_open=Keep open
|
||||
#search_dialog.tip_searching=Searching
|
||||
#search_dialog.limit_package=Limit to package:
|
||||
#search_dialog.res_text=Text
|
||||
#search_dialog.res_binary=Binary
|
||||
#search_dialog.package_not_found=No matching package found
|
||||
search_dialog.copy=copiar todo
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.resource=Sumber daya
|
||||
search_dialog.keep_open=Tetap terbuka
|
||||
search_dialog.tip_searching=Mencari
|
||||
#search_dialog.limit_package=Limit to package:
|
||||
#search_dialog.res_text=Text
|
||||
#search_dialog.res_binary=Binary
|
||||
#search_dialog.package_not_found=No matching package found
|
||||
search_dialog.copy=salin semua
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.resource=리소스
|
||||
search_dialog.keep_open=열어 두기
|
||||
search_dialog.tip_searching=검색 중...
|
||||
#search_dialog.limit_package=Limit to package:
|
||||
#search_dialog.res_text=Text
|
||||
#search_dialog.res_binary=Binary
|
||||
#search_dialog.package_not_found=No matching package found
|
||||
search_dialog.copy=모두 복사
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.resource=Recursos
|
||||
search_dialog.keep_open=Manter aberto
|
||||
search_dialog.tip_searching=Buscando
|
||||
#search_dialog.limit_package=Limit to package:
|
||||
#search_dialog.res_text=Text
|
||||
#search_dialog.res_binary=Binary
|
||||
#search_dialog.package_not_found=No matching package found
|
||||
search_dialog.copy=skopiuj wszystko
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.resource=Ресурсы
|
||||
search_dialog.keep_open=Оставлять поиск открытым
|
||||
search_dialog.tip_searching=Поиск...
|
||||
#search_dialog.limit_package=Limit to package:
|
||||
#search_dialog.res_text=Text
|
||||
#search_dialog.res_binary=Binary
|
||||
#search_dialog.package_not_found=No matching package found
|
||||
search_dialog.copy=скопировать все
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.resource=资源
|
||||
search_dialog.keep_open=保持窗口
|
||||
search_dialog.tip_searching=搜索中…
|
||||
search_dialog.limit_package=限制package:
|
||||
#search_dialog.res_text=Text
|
||||
#search_dialog.res_binary=Binary
|
||||
search_dialog.package_not_found=没有找到匹配的package
|
||||
search_dialog.copy=复制全部
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ search_dialog.resource=資源
|
||||
search_dialog.keep_open=保持開啟
|
||||
search_dialog.tip_searching=正在搜尋
|
||||
search_dialog.limit_package=限制至套件:
|
||||
#search_dialog.res_text=Text
|
||||
#search_dialog.res_binary=Binary
|
||||
search_dialog.package_not_found=找不到符合的套件
|
||||
search_dialog.copy=複製全部
|
||||
|
||||
|
||||
Reference in New Issue
Block a user