forked from hardcoresushi/DroidFS
Natural file name sorting
This commit is contained in:
parent
f15b17c936
commit
95eed07719
@ -151,7 +151,8 @@ Thanks to these open source projects that DroidFS uses:
|
|||||||
|
|
||||||
### Modified code:
|
### Modified code:
|
||||||
- [gocryptfs](https://github.com/rfjakob/gocryptfs) to encrypt your data
|
- [gocryptfs](https://github.com/rfjakob/gocryptfs) to encrypt your data
|
||||||
|
### Borrowed code:
|
||||||
|
- [MaterialFiles](https://github.com/zhanghai/MaterialFiles) for kotlin natural sorting implementation
|
||||||
### Libraries:
|
### Libraries:
|
||||||
- [Cyanea](https://github.com/jaredrummler/Cyanea) to customize UI
|
|
||||||
- [Glide](https://github.com/bumptech/glide/) to display pictures
|
- [Glide](https://github.com/bumptech/glide/) to display pictures
|
||||||
- [ExoPlayer](https://github.com/google/ExoPlayer) to play media files
|
- [ExoPlayer](https://github.com/google/ExoPlayer) to play media files
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Code borrowed from the awesome Material Files app (https://github.com/zhanghai/MaterialFiles)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||||
|
* All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sushi.hardcore.droidfs.collation
|
||||||
|
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class ByteString internal constructor(
|
||||||
|
private val bytes: ByteArray
|
||||||
|
) : Comparable<ByteString> {
|
||||||
|
|
||||||
|
fun borrowBytes(): ByteArray = bytes
|
||||||
|
|
||||||
|
private var stringCache: String? = null
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
// We are okay with the potential race condition here.
|
||||||
|
var string = stringCache
|
||||||
|
if (string == null) {
|
||||||
|
// String() uses replacement char instead of throwing exception.
|
||||||
|
string = String(bytes)
|
||||||
|
stringCache = string
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (javaClass != other?.javaClass) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
other as ByteString
|
||||||
|
return bytes contentEquals other.bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int = bytes.contentHashCode()
|
||||||
|
|
||||||
|
override fun compareTo(other: ByteString): Int = bytes.compareTo(other.bytes)
|
||||||
|
|
||||||
|
private fun ByteArray.compareTo(other: ByteArray): Int {
|
||||||
|
val size = size
|
||||||
|
val otherSize = other.size
|
||||||
|
for (index in 0 until min(size, otherSize)) {
|
||||||
|
val byte = this[index]
|
||||||
|
val otherByte = other[index]
|
||||||
|
val result = byte - otherByte
|
||||||
|
if (result != 0) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size - otherSize
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromBytes(bytes: ByteArray, start: Int = 0, end: Int = bytes.size): ByteString =
|
||||||
|
ByteString(bytes.copyOfRange(start, end))
|
||||||
|
|
||||||
|
fun fromString(string: String): ByteString =
|
||||||
|
ByteString(string.toByteArray()).apply { stringCache = string }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.toByteString(start: Int = 0, end: Int = size): ByteString =
|
||||||
|
ByteString.fromBytes(this, start, end)
|
||||||
|
|
||||||
|
fun String.toByteString(): ByteString = ByteString.fromString(this)
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Code borrowed from the awesome Material Files app (https://github.com/zhanghai/MaterialFiles)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||||
|
* All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sushi.hardcore.droidfs.collation
|
||||||
|
|
||||||
|
class ByteStringBuilder(capacity: Int = 16) {
|
||||||
|
private var bytes = ByteArray(capacity)
|
||||||
|
|
||||||
|
var length = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun append(byte: Byte): ByteStringBuilder {
|
||||||
|
ensureCapacity(length + 1)
|
||||||
|
bytes[length] = byte
|
||||||
|
++length
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun append(bytes: ByteArray, start: Int = 0, end: Int = bytes.size): ByteStringBuilder {
|
||||||
|
val newLength = length + (end - start)
|
||||||
|
ensureCapacity(newLength)
|
||||||
|
bytes.copyInto(this.bytes, length, start, end)
|
||||||
|
length = newLength
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun append(byteString: ByteString): ByteStringBuilder = append(byteString.borrowBytes())
|
||||||
|
|
||||||
|
private fun ensureCapacity(minimumCapacity: Int) {
|
||||||
|
val capacity = bytes.size
|
||||||
|
if (minimumCapacity > capacity) {
|
||||||
|
var newCapacity = (capacity shl 1) + 2
|
||||||
|
if (newCapacity < minimumCapacity) {
|
||||||
|
newCapacity = minimumCapacity
|
||||||
|
}
|
||||||
|
bytes = bytes.copyOf(newCapacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toByteString(): ByteString = bytes.toByteString(0, length)
|
||||||
|
|
||||||
|
override fun toString(): String = String(bytes, 0, length)
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Code borrowed from the awesome Material Files app (https://github.com/zhanghai/MaterialFiles)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||||
|
* All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sushi.hardcore.droidfs.collation
|
||||||
|
|
||||||
|
import java.text.CollationKey
|
||||||
|
import java.text.Collator
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
private val COLLATION_SENTINEL = byteArrayOf(1, 1, 1)
|
||||||
|
|
||||||
|
// @see https://github.com/GNOME/glib/blob/mainline/glib/gunicollate.c
|
||||||
|
// g_utf8_collate_key_for_filename()
|
||||||
|
fun Collator.getCollationKeyForFileName(source: String): CollationKey {
|
||||||
|
val result = ByteStringBuilder()
|
||||||
|
val suffix = ByteStringBuilder()
|
||||||
|
var previousIndex = 0
|
||||||
|
var index = 0
|
||||||
|
val endIndex = source.length
|
||||||
|
while (index < endIndex) {
|
||||||
|
when {
|
||||||
|
source[index] == '.' -> {
|
||||||
|
if (previousIndex != index) {
|
||||||
|
val collationKey = getCollationKey(source.substring(previousIndex, index))
|
||||||
|
result.append(collationKey.toByteArray())
|
||||||
|
}
|
||||||
|
result.append(COLLATION_SENTINEL).append(1)
|
||||||
|
previousIndex = index + 1
|
||||||
|
}
|
||||||
|
source[index].isAsciiDigit() -> {
|
||||||
|
if (previousIndex != index) {
|
||||||
|
val collationKey = getCollationKey(source.substring(previousIndex, index))
|
||||||
|
result.append(collationKey.toByteArray())
|
||||||
|
}
|
||||||
|
result.append(COLLATION_SENTINEL).append(2)
|
||||||
|
previousIndex = index
|
||||||
|
var leadingZeros: Int
|
||||||
|
var digits: Int
|
||||||
|
if (source[index] == '0') {
|
||||||
|
leadingZeros = 1
|
||||||
|
digits = 0
|
||||||
|
} else {
|
||||||
|
leadingZeros = 0
|
||||||
|
digits = 1
|
||||||
|
}
|
||||||
|
while (++index < endIndex) {
|
||||||
|
if (source[index] == '0' && digits == 0) {
|
||||||
|
++leadingZeros
|
||||||
|
} else if (source[index].isAsciiDigit()) {
|
||||||
|
++digits
|
||||||
|
} else {
|
||||||
|
if (digits == 0) {
|
||||||
|
++digits
|
||||||
|
--leadingZeros
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (digits > 1) {
|
||||||
|
result.append(':'.code.toByte())
|
||||||
|
--digits
|
||||||
|
}
|
||||||
|
if (leadingZeros > 0) {
|
||||||
|
suffix.append(leadingZeros.toByte())
|
||||||
|
previousIndex += leadingZeros
|
||||||
|
}
|
||||||
|
result.append(source.substring(previousIndex, index).toByteString())
|
||||||
|
previousIndex = index
|
||||||
|
--index
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
++index
|
||||||
|
}
|
||||||
|
if (previousIndex != index) {
|
||||||
|
val collationKey = getCollationKey(source.substring(previousIndex, index))
|
||||||
|
result.append(collationKey.toByteArray())
|
||||||
|
}
|
||||||
|
result.append(suffix.toByteString())
|
||||||
|
return ByteArrayCollationKey(source, result.toByteString().borrowBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Char.isAsciiDigit(): Boolean = this in '0'..'9'
|
||||||
|
|
||||||
|
private class ByteArrayCollationKey(
|
||||||
|
@Suppress("CanBeParameter")
|
||||||
|
private val source: String,
|
||||||
|
private val bytes: ByteArray
|
||||||
|
) : CollationKey(source) {
|
||||||
|
override fun compareTo(other: CollationKey): Int {
|
||||||
|
other as ByteArrayCollationKey
|
||||||
|
return bytes.unsignedCompareTo(other.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toByteArray(): ByteArray = bytes.copyOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.unsignedCompareTo(other: ByteArray): Int {
|
||||||
|
val size = size
|
||||||
|
val otherSize = other.size
|
||||||
|
for (index in 0 until min(size, otherSize)) {
|
||||||
|
val byte = this[index].toInt() and 0xFF
|
||||||
|
val otherByte = other[index].toInt() and 0xFF
|
||||||
|
if (byte < otherByte) {
|
||||||
|
return -1
|
||||||
|
} else if (byte > otherByte) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size - otherSize
|
||||||
|
}
|
@ -1,11 +1,14 @@
|
|||||||
package sushi.hardcore.droidfs.explorers
|
package sushi.hardcore.droidfs.explorers
|
||||||
|
|
||||||
|
import sushi.hardcore.droidfs.collation.getCollationKeyForFileName
|
||||||
import sushi.hardcore.droidfs.util.PathUtils
|
import sushi.hardcore.droidfs.util.PathUtils
|
||||||
|
import java.text.Collator
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ExplorerElement(val name: String, val elementType: Short, var size: Long = -1, mTime: Long = -1, val parentPath: String) {
|
class ExplorerElement(val name: String, val elementType: Short, var size: Long = -1, mTime: Long = -1, val parentPath: String) {
|
||||||
val mTime = Date((mTime * 1000).toString().toLong())
|
val mTime = Date((mTime * 1000).toString().toLong())
|
||||||
val fullPath: String = PathUtils.pathJoin(parentPath, name)
|
val fullPath: String = PathUtils.pathJoin(parentPath, name)
|
||||||
|
val collationKey = Collator.getInstance().getCollationKeyForFileName(fullPath)
|
||||||
|
|
||||||
val isDirectory: Boolean
|
val isDirectory: Boolean
|
||||||
get() = elementType.toInt() == DIRECTORY_TYPE
|
get() = elementType.toInt() == DIRECTORY_TYPE
|
||||||
@ -53,7 +56,7 @@ class ExplorerElement(val name: String, val elementType: Short, var size: Long =
|
|||||||
when (sortOrder) {
|
when (sortOrder) {
|
||||||
"name" -> {
|
"name" -> {
|
||||||
explorerElements.sortWith { a, b ->
|
explorerElements.sortWith { a, b ->
|
||||||
doSort(a, b, foldersFirst) { a.fullPath.compareTo(b.fullPath, true) }
|
doSort(a, b, foldersFirst) { a.collationKey.compareTo(b.collationKey) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"size" -> {
|
"size" -> {
|
||||||
@ -68,7 +71,7 @@ class ExplorerElement(val name: String, val elementType: Short, var size: Long =
|
|||||||
}
|
}
|
||||||
"name_desc" -> {
|
"name_desc" -> {
|
||||||
explorerElements.sortWith { a, b ->
|
explorerElements.sortWith { a, b ->
|
||||||
doSort(a, b, foldersFirst) { b.fullPath.compareTo(a.fullPath, true) }
|
doSort(a, b, foldersFirst) { b.collationKey.compareTo(a.collationKey) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"size_desc" -> {
|
"size_desc" -> {
|
||||||
|
Loading…
Reference in New Issue
Block a user