Thumbnails

This commit is contained in:
Matéo Duparc 2021-11-11 15:05:33 +01:00
parent e3df7be3b5
commit fd5ddc02b1
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
13 changed files with 180 additions and 122 deletions

View File

@ -91,7 +91,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false) usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
binding = ActivityCameraBinding.inflate(layoutInflater) binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
gocryptfsVolume = GocryptfsVolume(intent.getIntExtra("sessionID", -1)) gocryptfsVolume = GocryptfsVolume(applicationContext, intent.getIntExtra("sessionID", -1))
outputDirectory = intent.getStringExtra("path")!! outputDirectory = intent.getStringExtra("path")!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

View File

@ -38,14 +38,5 @@ class ConstValues {
fun isText(path: String): Boolean { fun isText(path: String): Boolean {
return isExtensionType("text", path) return isExtensionType("text", path)
} }
fun getAssociatedDrawable(path: String): Int {
return when {
isAudio(path) -> R.drawable.icon_file_audio
isImage(path) -> R.drawable.icon_file_image
isVideo(path) -> R.drawable.icon_file_video
isText(path) -> R.drawable.icon_file_text
else -> R.drawable.icon_file_unknown
}
}
} }
} }

View File

@ -167,7 +167,7 @@ class CreateActivity : VolumeActionActivity() {
super.onPause() super.onPause()
//Closing volume if leaving activity while showing dialog //Closing volume if leaving activity while showing dialog
if (sessionID != -1 && !isStartingExplorer) { if (sessionID != -1 && !isStartingExplorer) {
GocryptfsVolume(sessionID).close() GocryptfsVolume(applicationContext, sessionID).close()
finish() finish()
} }
} }

View File

@ -9,7 +9,7 @@ import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
class GocryptfsVolume(var sessionID: Int) { class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) {
private external fun native_close(sessionID: Int) private external fun native_close(sessionID: Int)
private external fun native_is_closed(sessionID: Int): Boolean private external fun native_is_closed(sessionID: Int): Boolean
private external fun native_list_dir(sessionID: Int, dir_path: String): MutableList<ExplorerElement> private external fun native_list_dir(sessionID: Int, dir_path: String): MutableList<ExplorerElement>
@ -47,79 +47,79 @@ class GocryptfsVolume(var sessionID: Int) {
} }
fun close() { fun close() {
synchronized(this){ synchronized(applicationContext) {
native_close(sessionID) native_close(sessionID)
} }
} }
fun isClosed(): Boolean { fun isClosed(): Boolean {
synchronized(this){ synchronized(applicationContext) {
return native_is_closed(sessionID) return native_is_closed(sessionID)
} }
} }
fun listDir(dir_path: String): MutableList<ExplorerElement> { fun listDir(dir_path: String): MutableList<ExplorerElement> {
synchronized(this){ synchronized(applicationContext) {
return native_list_dir(sessionID, dir_path) return native_list_dir(sessionID, dir_path)
} }
} }
fun mkdir(dir_path: String): Boolean { fun mkdir(dir_path: String): Boolean {
synchronized(this){ synchronized(applicationContext) {
return native_mkdir(sessionID, dir_path, ConstValues.DIRECTORY_MODE) return native_mkdir(sessionID, dir_path, ConstValues.DIRECTORY_MODE)
} }
} }
fun rmdir(dir_path: String): Boolean { fun rmdir(dir_path: String): Boolean {
synchronized(this){ synchronized(applicationContext) {
return native_rmdir(sessionID, dir_path) return native_rmdir(sessionID, dir_path)
} }
} }
fun removeFile(file_path: String): Boolean { fun removeFile(file_path: String): Boolean {
synchronized(this){ synchronized(applicationContext) {
return native_remove_file(sessionID, file_path) return native_remove_file(sessionID, file_path)
} }
} }
fun pathExists(file_path: String): Boolean { fun pathExists(file_path: String): Boolean {
synchronized(this){ synchronized(applicationContext) {
return native_path_exists(sessionID, file_path) return native_path_exists(sessionID, file_path)
} }
} }
fun getSize(file_path: String): Long { fun getSize(file_path: String): Long {
synchronized(this){ synchronized(applicationContext) {
return native_get_size(sessionID, file_path) return native_get_size(sessionID, file_path)
} }
} }
fun closeFile(handleID: Int) { fun closeFile(handleID: Int) {
synchronized(this){ synchronized(applicationContext) {
native_close_file(sessionID, handleID) native_close_file(sessionID, handleID)
} }
} }
fun openReadMode(file_path: String): Int { fun openReadMode(file_path: String): Int {
synchronized(this){ synchronized(applicationContext) {
return native_open_read_mode(sessionID, file_path) return native_open_read_mode(sessionID, file_path)
} }
} }
fun openWriteMode(file_path: String): Int { fun openWriteMode(file_path: String): Int {
synchronized(this){ synchronized(applicationContext) {
return native_open_write_mode(sessionID, file_path, ConstValues.FILE_MODE) return native_open_write_mode(sessionID, file_path, ConstValues.FILE_MODE)
} }
} }
fun readFile(handleID: Int, offset: Long, buff: ByteArray): Int { fun readFile(handleID: Int, offset: Long, buff: ByteArray): Int {
synchronized(this){ synchronized(applicationContext) {
return native_read_file(sessionID, handleID, offset, buff) return native_read_file(sessionID, handleID, offset, buff)
} }
} }
fun writeFile(handleID: Int, offset: Long, buff: ByteArray, buff_size: Int): Int { fun writeFile(handleID: Int, offset: Long, buff: ByteArray, buff_size: Int): Int {
synchronized(this){ synchronized(applicationContext) {
return native_write_file(sessionID, handleID, offset, buff, buff_size) return native_write_file(sessionID, handleID, offset, buff, buff_size)
} }
} }
@ -237,4 +237,40 @@ class GocryptfsVolume(var sessionID: Int) {
null null
} }
} }
fun loadWholeFile(fullPath: String, maxSize: Long? = null): Pair<ByteArray?, Int> {
val fileSize = getSize(fullPath)
return if (fileSize >= 0) {
maxSize?.let {
if (fileSize > it) {
return Pair(null, 0)
}
}
try {
val fileBuff = ByteArray(fileSize.toInt())
val handleID = openReadMode(fullPath)
if (handleID == -1) {
Pair(null, 3)
} else {
var offset: Long = 0
val ioBuffer = ByteArray(DefaultBS)
var length: Int
while (readFile(handleID, offset, ioBuffer).also { length = it } > 0) {
System.arraycopy(ioBuffer, 0, fileBuff, offset.toInt(), length)
offset += length.toLong()
}
closeFile(handleID)
if (offset == fileBuff.size.toLong()) {
Pair(fileBuff, 0)
} else {
Pair(null, 4)
}
}
} catch (e: OutOfMemoryError) {
Pair(null, 2)
}
} else {
Pair(null, 1)
}
}
} }

View File

@ -170,7 +170,7 @@ class OpenActivity : VolumeActionActivity() {
if (success){ if (success){
startExplorer() startExplorer()
} else { } else {
GocryptfsVolume(sessionID).close() GocryptfsVolume(applicationContext, sessionID).close()
} }
} }
} }
@ -257,7 +257,7 @@ class OpenActivity : VolumeActionActivity() {
if (intent.action == "pick" && !isFinishingIntentionally){ if (intent.action == "pick" && !isFinishingIntentionally){
val sessionID = intent.getIntExtra("sessionID", -1) val sessionID = intent.getIntExtra("sessionID", -1)
if (sessionID != -1){ if (sessionID != -1){
GocryptfsVolume(sessionID).close() GocryptfsVolume(applicationContext, sessionID).close()
RestrictedFileProvider.wipeAll(this) RestrictedFileProvider.wipeAll(this)
} }
} }

View File

@ -1,14 +1,18 @@
package sushi.hardcore.droidfs.adapters package sushi.hardcore.droidfs.adapters
import android.content.Context import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.droidfs.ConstValues.Companion.getAssociatedDrawable import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.transition.Transition
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
@ -16,17 +20,17 @@ import java.text.DateFormat
import java.util.* import java.util.*
class ExplorerElementAdapter( class ExplorerElementAdapter(
context: Context, val activity: AppCompatActivity,
private val onExplorerElementClick: (Int) -> Unit, val gocryptfsVolume: GocryptfsVolume?,
private val onExplorerElementLongClick: (Int) -> Unit val onExplorerElementClick: (Int) -> Unit,
val onExplorerElementLongClick: (Int) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault()) val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault())
var explorerElements = listOf<ExplorerElement>() var explorerElements = listOf<ExplorerElement>()
set(value) { set(value) {
field = value field = value
unSelectAll() unSelectAll()
} }
private val inflater: LayoutInflater = LayoutInflater.from(context)
val selectedItems: MutableList<Int> = ArrayList() val selectedItems: MutableList<Int> = ArrayList()
override fun getItemCount(): Int { override fun getItemCount(): Int {
@ -72,11 +76,7 @@ class ExplorerElementAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
open class ExplorerElementViewHolder( open class ExplorerElementViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
itemView: View,
private val onClick: (Int) -> Boolean,
private val onLongClick: (Int) -> Boolean,
) : RecyclerView.ViewHolder(itemView) {
private val textElementName by lazy { private val textElementName by lazy {
itemView.findViewById<TextView>(R.id.text_element_name) itemView.findViewById<TextView>(R.id.text_element_name)
} }
@ -99,57 +99,87 @@ class ExplorerElementAdapter(
open fun bind(explorerElement: ExplorerElement, position: Int) { open fun bind(explorerElement: ExplorerElement, position: Int) {
textElementName.text = explorerElement.name textElementName.text = explorerElement.name
selectableContainer.setOnClickListener { (bindingAdapter as ExplorerElementAdapter?)?.let { adapter ->
setBackground(onClick(position)) selectableContainer.setOnClickListener {
} setBackground(adapter.onItemClick(position))
selectableContainer.setOnLongClickListener { }
setBackground(onLongClick(position)) selectableContainer.setOnLongClickListener {
true setBackground(adapter.onItemLongClick(position))
true
}
} }
} }
} }
open class RegularElementViewHolder( open class RegularElementViewHolder(itemView: View) : ExplorerElementViewHolder(itemView) {
itemView: View,
private val dateFormat: DateFormat,
onClick: (Int) -> Boolean,
onLongClick: (Int) -> Boolean,
) : ExplorerElementViewHolder(itemView, onClick, onLongClick) {
open fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) { open fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) {
super.bind(explorerElement, position) super.bind(explorerElement, position)
textElementSize.text = PathUtils.formatSize(explorerElement.size) textElementSize.text = PathUtils.formatSize(explorerElement.size)
textElementMtime.text = dateFormat.format(explorerElement.mTime) (bindingAdapter as ExplorerElementAdapter?)?.let {
textElementMtime.text = it.dateFormat.format(explorerElement.mTime)
}
setBackground(isSelected) setBackground(isSelected)
} }
} }
class FileViewHolder( class FileViewHolder(itemView: View) : RegularElementViewHolder(itemView) {
itemView: View, var displayThumbnail = true
dateFormat: DateFormat, var target: DrawableImageViewTarget? = null
onClick: (Int) -> Boolean,
onLongClick: (Int) -> Boolean, private fun loadThumbnail(fullPath: String) {
) : RegularElementViewHolder(itemView, dateFormat, onClick, onLongClick) { (bindingAdapter as ExplorerElementAdapter?)?.let { adapter ->
adapter.gocryptfsVolume?.let { volume ->
displayThumbnail = true
Thread {
volume.loadWholeFile(fullPath, 50_000_000).first?.let {
if (displayThumbnail) {
adapter.activity.runOnUiThread {
if (displayThumbnail) {
target = Glide.with(adapter.activity).load(it).into(object : DrawableImageViewTarget(icon) {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
super.onResourceReady(resource, transition)
target = null
}
})
}
}
}
}
}.start()
}
}
}
override fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) { override fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) {
super.bind(explorerElement, position, isSelected) super.bind(explorerElement, position, isSelected)
icon.setImageResource(getAssociatedDrawable(explorerElement.name)) icon.setImageResource(
when {
ConstValues.isImage(explorerElement.name) -> {
loadThumbnail(explorerElement.fullPath)
R.drawable.icon_file_image
}
ConstValues.isVideo(explorerElement.name) -> {
loadThumbnail(explorerElement.fullPath)
R.drawable.icon_file_video
}
ConstValues.isText(explorerElement.name) -> R.drawable.icon_file_text
ConstValues.isAudio(explorerElement.name) -> R.drawable.icon_file_audio
else -> R.drawable.icon_file_unknown
}
)
} }
} }
class DirectoryViewHolder(
itemView: View, class DirectoryViewHolder(itemView: View) : RegularElementViewHolder(itemView) {
dateFormat: DateFormat,
onClick: (Int) -> Boolean,
onLongClick: (Int) -> Boolean,
) : RegularElementViewHolder(itemView, dateFormat, onClick, onLongClick) {
override fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) { override fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) {
super.bind(explorerElement, position, isSelected) super.bind(explorerElement, position, isSelected)
icon.setImageResource(R.drawable.icon_folder) icon.setImageResource(R.drawable.icon_folder)
} }
} }
class ParentFolderViewHolder(
itemView: View, class ParentFolderViewHolder(itemView: View): ExplorerElementViewHolder(itemView) {
onClick: (Int) -> Boolean,
onLongClick: (Int) -> Boolean,
): ExplorerElementViewHolder(itemView, onClick, onLongClick) {
override fun bind(explorerElement: ExplorerElement, position: Int) { override fun bind(explorerElement: ExplorerElement, position: Int) {
super.bind(explorerElement, position) super.bind(explorerElement, position)
textElementSize.text = "" textElementSize.text = ""
@ -158,12 +188,22 @@ class ExplorerElementAdapter(
} }
} }
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
if (holder is FileViewHolder) {
//cancel pending thumbnail display
holder.displayThumbnail = false
holder.target?.let {
Glide.with(activity).clear(it)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = inflater.inflate(R.layout.adapter_explorer_element, parent, false) val view = activity.layoutInflater.inflate(R.layout.adapter_explorer_element, parent, false)
return when (viewType) { return when (viewType) {
ExplorerElement.REGULAR_FILE_TYPE -> FileViewHolder(view, dateFormat, ::onItemClick, ::onItemLongClick) ExplorerElement.REGULAR_FILE_TYPE -> FileViewHolder(view)
ExplorerElement.DIRECTORY_TYPE -> DirectoryViewHolder(view, dateFormat, ::onItemClick, ::onItemLongClick) ExplorerElement.DIRECTORY_TYPE -> DirectoryViewHolder(view)
ExplorerElement.PARENT_FOLDER_TYPE -> ParentFolderViewHolder(view, ::onItemClick, ::onItemLongClick) ExplorerElement.PARENT_FOLDER_TYPE -> ParentFolderViewHolder(view)
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }

View File

@ -78,7 +78,7 @@ open class BaseExplorerActivity : BaseActivity() {
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false) usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
volumeName = intent.getStringExtra("volume_name") ?: "" volumeName = intent.getStringExtra("volume_name") ?: ""
val sessionID = intent.getIntExtra("sessionID", -1) val sessionID = intent.getIntExtra("sessionID", -1)
gocryptfsVolume = GocryptfsVolume(sessionID) gocryptfsVolume = GocryptfsVolume(applicationContext, sessionID)
sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries) sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries)
sortOrderValues = resources.getStringArray(R.array.sort_orders_values) sortOrderValues = resources.getStringArray(R.array.sort_orders_values)
foldersFirst = sharedPrefs.getBoolean("folders_first", true) foldersFirst = sharedPrefs.getBoolean("folders_first", true)
@ -95,7 +95,16 @@ open class BaseExplorerActivity : BaseActivity() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
title = "" title = ""
titleText.text = getString(R.string.volume, volumeName) titleText.text = getString(R.string.volume, volumeName)
explorerAdapter = ExplorerElementAdapter(this, ::onExplorerItemClick, ::onExplorerItemLongClick) explorerAdapter = ExplorerElementAdapter(
this,
if (sharedPrefs.getBoolean("thumbnails", true)) {
gocryptfsVolume
} else {
null
},
::onExplorerItemClick,
::onExplorerItemLongClick
)
explorerViewModel= ViewModelProvider(this).get(ExplorerViewModel::class.java) explorerViewModel= ViewModelProvider(this).get(ExplorerViewModel::class.java)
currentDirectoryPath = explorerViewModel.currentDirectoryPath currentDirectoryPath = explorerViewModel.currentDirectoryPath
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)

View File

@ -36,7 +36,7 @@ class ExplorerActivity : BaseExplorerActivity() {
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.let { resultIntent -> result.data?.let { resultIntent ->
val remoteSessionID = resultIntent.getIntExtra("sessionID", -1) val remoteSessionID = resultIntent.getIntExtra("sessionID", -1)
val remoteGocryptfsVolume = GocryptfsVolume(remoteSessionID) val remoteGocryptfsVolume = GocryptfsVolume(applicationContext, remoteSessionID)
val path = resultIntent.getStringExtra("path") val path = resultIntent.getStringExtra("path")
val operationFiles = ArrayList<OperationFile>() val operationFiles = ArrayList<OperationFile>()
if (path == null){ //multiples elements if (path == null){ //multiples elements

View File

@ -86,8 +86,7 @@ class ExplorerActivityPick : BaseExplorerActivity() {
if (!isFinishingIntentionally && !usf_keep_open){ if (!isFinishingIntentionally && !usf_keep_open){
val sessionID = intent.getIntExtra("originalSessionID", -1) val sessionID = intent.getIntExtra("originalSessionID", -1)
if (sessionID != -1){ if (sessionID != -1){
val v = GocryptfsVolume(sessionID) GocryptfsVolume(applicationContext, sessionID).close()
v.close()
} }
super.closeVolumeOnDestroy() super.closeVolumeOnDestroy()
} }

View File

@ -30,7 +30,7 @@ abstract class FileViewerActivity: BaseActivity() {
filePath = intent.getStringExtra("path")!! filePath = intent.getStringExtra("path")!!
originalParentPath = PathUtils.getParentPath(filePath) originalParentPath = PathUtils.getParentPath(filePath)
val sessionID = intent.getIntExtra("sessionID", -1) val sessionID = intent.getIntExtra("sessionID", -1)
gocryptfsVolume = GocryptfsVolume(sessionID) gocryptfsVolume = GocryptfsVolume(applicationContext, sessionID)
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false) usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
foldersFirst = sharedPrefs.getBoolean("folders_first", true) foldersFirst = sharedPrefs.getBoolean("folders_first", true)
windowInsetsController = WindowInsetsControllerCompat(window, window.decorView) windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
@ -56,51 +56,20 @@ abstract class FileViewerActivity: BaseActivity() {
} }
protected fun loadWholeFile(path: String): ByteArray? { protected fun loadWholeFile(path: String): ByteArray? {
val fileSize = gocryptfsVolume.getSize(path) val result = gocryptfsVolume.loadWholeFile(path)
if (fileSize >= 0){ if (result.second != 0) {
try { val dialog = CustomAlertDialogBuilder(this, themeValue)
val fileBuff = ByteArray(fileSize.toInt())
var success = false
val handleID = gocryptfsVolume.openReadMode(path)
if (handleID != -1) {
var offset: Long = 0
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (gocryptfsVolume.readFile(handleID, offset, ioBuffer).also { length = it } > 0){
System.arraycopy(ioBuffer, 0, fileBuff, offset.toInt(), length)
offset += length.toLong()
}
gocryptfsVolume.closeFile(handleID)
success = offset == fileBuff.size.toLong()
}
if (success){
return fileBuff
} else {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.read_file_failed)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer() }
.show()
}
} catch (e: OutOfMemoryError){
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.outofmemoryerror_msg)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer() }
.show()
}
} else {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.get_size_failed)
.setCancelable(false) .setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer() } .setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer() }
.show() when (result.second) {
1 -> dialog.setMessage(R.string.get_size_failed)
2 -> dialog.setMessage(R.string.outofmemoryerror_msg)
else -> dialog.setMessage(R.string.read_file_failed)
}
dialog.show()
} }
return null return result.first
} }
protected fun createPlaylist() { protected fun createPlaylist() {

View File

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/colorAccent" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@ -204,4 +204,6 @@
<string name="encryption_cipher_label">Encryption cipher:</string> <string name="encryption_cipher_label">Encryption cipher:</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="theme_summary">Customize app theme</string> <string name="theme_summary">Customize app theme</string>
<string name="thumbnails">Thumbnails</string>
<string name="thumbnails_summary">Show images and videos thumbnails</string>
</resources> </resources>

View File

@ -38,6 +38,13 @@
android:title="@string/folders_first" android:title="@string/folders_first"
android:summary="@string/folders_first_summary"/> android:summary="@string/folders_first_summary"/>
<SwitchPreferenceCompat
android:defaultValue="true"
android:icon="@drawable/icon_image"
android:key="thumbnails"
android:title="@string/thumbnails"
android:summary="@string/thumbnails_summary"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
app:icon="@drawable/icon_folder_search" app:icon="@drawable/icon_folder_search"