List files in human order (Original Files area only)
@see https://www.filebot.net/forums/viewtopic.php?f=10&t=4174 @see http://stackoverflow.com/questions/12640280/looking-for-a-combination-of-alphabetical-and-natural-order-aka-user-sane-sort
This commit is contained in:
parent
477aa7019c
commit
b1a30e4bc3
|
@ -297,7 +297,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getQueryInputMessage(String header, String message, Collection<File> files) throws Exception {
|
protected String getQueryInputMessage(String header, String message, Collection<File> files) throws Exception {
|
||||||
List<File> selection = files.stream().sorted(comparing(File::length).reversed()).limit(5).collect(toList());
|
List<File> selection = files.stream().sorted(comparing(File::length).reversed()).limit(5).sorted(HUMAN_ORDER).collect(toList());
|
||||||
if (selection.isEmpty()) {
|
if (selection.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -309,7 +309,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
TextColorizer colorizer = new TextColorizer("<nobr>• ", "</nobr><br>");
|
TextColorizer colorizer = new TextColorizer("<nobr>• ", "</nobr><br>");
|
||||||
for (File file : sortByUniquePath(selection)) {
|
for (File file : selection) {
|
||||||
File path = getStructurePathTail(file);
|
File path = getStructurePathTail(file);
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
path = getRelativePathTail(file, 3);
|
path = getRelativePathTail(file, 3);
|
||||||
|
|
|
@ -5,14 +5,15 @@ import static java.util.stream.Collectors.*;
|
||||||
import static net.filebot.Logging.*;
|
import static net.filebot.Logging.*;
|
||||||
import static net.filebot.MediaTypes.*;
|
import static net.filebot.MediaTypes.*;
|
||||||
import static net.filebot.util.FileUtilities.*;
|
import static net.filebot.util.FileUtilities.*;
|
||||||
|
import static net.filebot.util.RegularExpressions.*;
|
||||||
|
|
||||||
import java.awt.datatransfer.Transferable;
|
import java.awt.datatransfer.Transferable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import net.filebot.media.MediaDetection;
|
import net.filebot.media.MediaDetection;
|
||||||
|
@ -50,26 +51,21 @@ class FilesListTransferablePolicy extends BackgroundFileTransferablePolicy<File>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void load(List<File> files, TransferAction action) {
|
protected void load(List<File> files, TransferAction action) {
|
||||||
Set<File> fileset = new LinkedHashSet<File>();
|
Set<File> fileset = new TreeSet<File>(CASE_INSENSITIVE_ORDER);
|
||||||
|
|
||||||
// load files recursively by default
|
// load files recursively by default
|
||||||
load(files, action != TransferAction.LINK, fileset);
|
load(files, action != TransferAction.LINK, fileset);
|
||||||
|
|
||||||
// use fast file to minimize system calls like length(), isDirectory(), isFile(), ...
|
// use fast file to minimize system calls like length(), isDirectory(), isFile(), ... and list files in human order
|
||||||
publish(fileset.stream().map(FastFile::new).toArray(File[]::new));
|
publish(fileset.stream().sorted(HUMAN_ORDER).map(FastFile::new).toArray(File[]::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void load(List<File> files, boolean recursive, Collection<File> sink) {
|
private void load(List<File> files, boolean recursive, Collection<File> sink) {
|
||||||
for (File f : files) {
|
for (File f : files) {
|
||||||
// ignore hidden files
|
|
||||||
if (f.isHidden()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load file paths from text files
|
// load file paths from text files
|
||||||
if (recursive && LIST_FILES.accept(f)) {
|
if (recursive && LIST_FILES.accept(f)) {
|
||||||
try {
|
try {
|
||||||
String[] lines = readTextFile(f).split("\\R");
|
String[] lines = NEWLINE.split(readTextFile(f));
|
||||||
List<File> paths = stream(lines).filter(s -> s.length() > 0).map(path -> {
|
List<File> paths = stream(lines).filter(s -> s.length() > 0).map(path -> {
|
||||||
try {
|
try {
|
||||||
File file = new File(path);
|
File file = new File(path);
|
||||||
|
@ -96,7 +92,7 @@ class FilesListTransferablePolicy extends BackgroundFileTransferablePolicy<File>
|
||||||
|
|
||||||
// load folders recursively
|
// load folders recursively
|
||||||
else if (f.isDirectory()) {
|
else if (f.isDirectory()) {
|
||||||
load(sortByUniquePath(getChildren(f)), true, sink); // FORCE NATURAL FILE ORDER
|
load(getChildren(f, NOT_HIDDEN), true, sink); // FORCE NATURAL FILE ORDER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy<C
|
||||||
// handle single folder drop
|
// handle single folder drop
|
||||||
if (files.size() == 1 && containsOnly(files, FOLDERS)) {
|
if (files.size() == 1 && containsOnly(files, FOLDERS)) {
|
||||||
for (File folder : files) {
|
for (File folder : files) {
|
||||||
for (File file : getChildren(folder, NOT_HIDDEN, CASE_INSENSITIVE_PATH)) {
|
for (File file : getChildren(folder, NOT_HIDDEN, CASE_INSENSITIVE_ORDER)) {
|
||||||
load(file, null, folder);
|
load(file, null, folder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy<C
|
||||||
|
|
||||||
if (absoluteFile.isDirectory()) {
|
if (absoluteFile.isDirectory()) {
|
||||||
// load all files in the file tree
|
// load all files in the file tree
|
||||||
for (File child : getChildren(absoluteFile, NOT_HIDDEN, CASE_INSENSITIVE_PATH)) {
|
for (File child : getChildren(absoluteFile, NOT_HIDDEN, CASE_INSENSITIVE_ORDER)) {
|
||||||
load(child, relativeFile, root);
|
load(child, relativeFile, root);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
package net.filebot.util;
|
||||||
|
|
||||||
|
import java.text.Collator;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class AlphanumComparator implements Comparator<String> {
|
||||||
|
|
||||||
|
protected Collator collator;
|
||||||
|
|
||||||
|
public AlphanumComparator(Collator collator) {
|
||||||
|
this.collator = collator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlphanumComparator(Locale locale) {
|
||||||
|
this.collator = Collator.getInstance(locale);
|
||||||
|
this.collator.setDecomposition(Collator.FULL_DECOMPOSITION);
|
||||||
|
this.collator.setStrength(Collator.PRIMARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isDigit(String s, int i) {
|
||||||
|
return Character.isDigit(s.charAt(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getNumericValue(String s, int i) {
|
||||||
|
return Character.getNumericValue(s.charAt(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getChunk(String s, int start) {
|
||||||
|
int index = start;
|
||||||
|
int length = s.length();
|
||||||
|
boolean mode = isDigit(s, index++);
|
||||||
|
|
||||||
|
while (index < length) {
|
||||||
|
if (mode != isDigit(s, index)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.substring(start, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int compare(String s1, String s2) {
|
||||||
|
int length1 = s1.length();
|
||||||
|
int length2 = s2.length();
|
||||||
|
int index1 = 0;
|
||||||
|
int index2 = 0;
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
while (result == 0 && index1 < length1 && index2 < length2) {
|
||||||
|
String chunk1 = getChunk(s1, index1);
|
||||||
|
index1 += chunk1.length();
|
||||||
|
|
||||||
|
String chunk2 = getChunk(s2, index2);
|
||||||
|
index2 += chunk2.length();
|
||||||
|
|
||||||
|
if (isDigit(chunk1, 0) && isDigit(chunk2, 0)) {
|
||||||
|
int chunkLength1 = chunk1.length();
|
||||||
|
int chunkLength2 = chunk2.length();
|
||||||
|
|
||||||
|
// count and skip leading zeros
|
||||||
|
int zeroIndex1 = 0;
|
||||||
|
while (zeroIndex1 < chunkLength1 && getNumericValue(chunk1, zeroIndex1) == 0) {
|
||||||
|
++zeroIndex1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// count and skip leading zeros
|
||||||
|
int zeroIndex2 = 0;
|
||||||
|
while (zeroIndex2 < chunkLength2 && getNumericValue(chunk2, zeroIndex2) == 0) {
|
||||||
|
++zeroIndex2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the longer run of non-zero digits is greater
|
||||||
|
result = (chunkLength1 - zeroIndex1) - (chunkLength2 - zeroIndex2);
|
||||||
|
|
||||||
|
// if the length is the same, the first differing digit decides
|
||||||
|
// which one is deemed greater.
|
||||||
|
int numberIndex1 = zeroIndex1;
|
||||||
|
int numberIndex2 = zeroIndex2;
|
||||||
|
|
||||||
|
while (result == 0 && numberIndex1 < chunkLength1 && numberIndex2 < chunkLength2) {
|
||||||
|
result = getNumericValue(chunk1, numberIndex1++) - getNumericValue(chunk2, numberIndex2++);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if still no difference, the longer zeros-prefix is greater
|
||||||
|
if (result == 0) {
|
||||||
|
result = numberIndex1 - numberIndex2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = collator.compare(chunk1, chunk2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there was no difference at all, let the longer one be the greater one
|
||||||
|
if (result == 0) {
|
||||||
|
result = length1 - length2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// limit result to (-1, 0, or 1)
|
||||||
|
return Integer.signum(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package net.filebot.util;
|
||||||
import static java.nio.charset.StandardCharsets.*;
|
import static java.nio.charset.StandardCharsets.*;
|
||||||
import static java.util.Arrays.*;
|
import static java.util.Arrays.*;
|
||||||
import static java.util.Collections.*;
|
import static java.util.Collections.*;
|
||||||
|
import static java.util.Comparator.*;
|
||||||
import static java.util.stream.Collectors.*;
|
import static java.util.stream.Collectors.*;
|
||||||
import static net.filebot.Logging.*;
|
import static net.filebot.Logging.*;
|
||||||
import static net.filebot.util.RegularExpressions.*;
|
import static net.filebot.util.RegularExpressions.*;
|
||||||
|
@ -39,6 +40,7 @@ import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
@ -363,7 +365,7 @@ public final class FileUtilities {
|
||||||
|
|
||||||
public static List<File> sortByUniquePath(Collection<File> files) {
|
public static List<File> sortByUniquePath(Collection<File> files) {
|
||||||
// sort by unique lower-case paths
|
// sort by unique lower-case paths
|
||||||
TreeSet<File> sortedSet = new TreeSet<File>(CASE_INSENSITIVE_PATH);
|
TreeSet<File> sortedSet = new TreeSet<File>(CASE_INSENSITIVE_ORDER);
|
||||||
sortedSet.addAll(files);
|
sortedSet.addAll(files);
|
||||||
|
|
||||||
return new ArrayList<File>(sortedSet);
|
return new ArrayList<File>(sortedSet);
|
||||||
|
@ -385,7 +387,7 @@ public final class FileUtilities {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FileFilter not(FileFilter filter) {
|
public static FileFilter not(FileFilter filter) {
|
||||||
return new NotFileFilter(filter);
|
return f -> !filter.accept(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<File> listPath(File file) {
|
public static List<File> listPath(File file) {
|
||||||
|
@ -436,14 +438,14 @@ public final class FileUtilities {
|
||||||
return getChildren(folder, filter, null);
|
return getChildren(folder, filter, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<File> getChildren(File folder, FileFilter filter, Comparator<File> sorter) {
|
public static List<File> getChildren(File folder, FileFilter filter, Comparator<File> order) {
|
||||||
File[] files = filter == null ? folder.listFiles() : folder.listFiles(filter);
|
File[] files = filter == null ? folder.listFiles() : folder.listFiles(filter);
|
||||||
|
|
||||||
// children array may be null if folder permissions do not allow listing of files
|
// children array may be null if folder permissions do not allow listing of files
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
files = new File[0];
|
files = new File[0];
|
||||||
} else if (sorter != null) {
|
} else if (order != null) {
|
||||||
sort(files, sorter);
|
sort(files, order);
|
||||||
}
|
}
|
||||||
|
|
||||||
return asList(files);
|
return asList(files);
|
||||||
|
@ -667,21 +669,11 @@ public final class FileUtilities {
|
||||||
return String.format("%,d bytes", size);
|
return String.format("%,d bytes", size);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final FileFilter FOLDERS = new FileFilter() {
|
public static final FileFilter FOLDERS = File::isDirectory;
|
||||||
|
|
||||||
@Override
|
public static final FileFilter FILES = File::isFile;
|
||||||
public boolean accept(File file) {
|
|
||||||
return file.isDirectory();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final FileFilter FILES = new FileFilter() {
|
public static final FileFilter NOT_HIDDEN = not(File::isHidden);
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean accept(File file) {
|
|
||||||
return file.isFile();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final FileFilter TEMPORARY = new FileFilter() {
|
public static final FileFilter TEMPORARY = new FileFilter() {
|
||||||
|
|
||||||
|
@ -689,15 +681,7 @@ public final class FileUtilities {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean accept(File file) {
|
public boolean accept(File file) {
|
||||||
return file.getAbsolutePath().startsWith(tmpdir);
|
return file.getPath().startsWith(tmpdir);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final FileFilter NOT_HIDDEN = new FileFilter() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean accept(File file) {
|
|
||||||
return !file.isHidden();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -801,27 +785,9 @@ public final class FileUtilities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class NotFileFilter implements FileFilter {
|
public static final Comparator<File> CASE_INSENSITIVE_ORDER = comparing(File::getPath, String.CASE_INSENSITIVE_ORDER);
|
||||||
|
|
||||||
public FileFilter filter;
|
public static final Comparator<File> HUMAN_ORDER = comparing(File::getPath, new AlphanumComparator(Locale.ENGLISH));
|
||||||
|
|
||||||
public NotFileFilter(FileFilter filter) {
|
|
||||||
this.filter = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean accept(File file) {
|
|
||||||
return !filter.accept(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Comparator<File> CASE_INSENSITIVE_PATH = new Comparator<File>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compare(File o1, File o2) {
|
|
||||||
return o1.getPath().compareToIgnoreCase(o2.getPath());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dummy constructor to prevent instantiation.
|
* Dummy constructor to prevent instantiation.
|
||||||
|
|
Loading…
Reference in New Issue