diff --git a/app/build.gradle b/app/build.gradle
index 5e0481475..c7b570972 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
- versionCode 555
- versionName '5.2.3'
+ versionCode 556
+ versionName '5.3'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 26ab7650a..2b2c8e6c4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -105,6 +105,9 @@
+
= runInterruptible(Dispatchers.IO) {
+ getAvailableStorageDirs()
+ }
+
suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
uri.resolveFile(context)
}
+ suspend fun setDirIsNoMedia(dir: File) = runInterruptible(Dispatchers.IO) {
+ File(dir, NOMEDIA).createNewFile()
+ }
+
fun takePermissions(uri: Uri) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, flags)
}
- @Deprecated("")
- fun getStorageDisplayName(file: File) = file.getStorageName(context)
-
suspend fun getDirectoryDisplayName(dir: File, isFullPath: Boolean): String = runInterruptible(Dispatchers.IO) {
val packageName = context.packageName
if (dir.absolutePath.contains(packageName)) {
@@ -104,9 +110,6 @@ class LocalStorageManager @Inject constructor(
private fun getConfiguredStorageDirs(): MutableSet {
val set = getAvailableStorageDirs()
set.addAll(settings.userSpecifiedMangaDirectories)
- settings.mangaStorageDir?.let {
- set.add(it)
- }
return set
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
index 0b2d0ef5e..d02a89b92 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
@@ -33,7 +33,7 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
- addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick))
+ addMenuProvider(LocalListMenuProvider(binding.root.context, this::onEmptyActionClick))
viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() }
}
@@ -45,7 +45,7 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
FilterSheetFragment.show(childFragmentManager)
}
- override fun onScrolledToEnd() = Unit
+ override fun onScrolledToEnd() = viewModel.loadNextPage()
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_local, menu)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt
index f2be49775..bdceb0328 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt
@@ -1,12 +1,15 @@
package org.koitharu.kotatsu.local.ui
+import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
class LocalListMenuProvider(
+ private val context: Context,
private val onImportClick: Function0,
) : MenuProvider {
@@ -20,6 +23,12 @@ class LocalListMenuProvider(
onImportClick()
true
}
+
+ R.id.action_settings -> {
+ context.startActivity(MangaDirectoriesActivity.newIntent(context))
+ true
+ }
+
else -> false
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
index 99723036a..04b7ed947 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.local.ui
+import android.content.SharedPreferences
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -24,7 +25,7 @@ class LocalListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
filter: FilterCoordinator,
- settings: AppSettings,
+ private val settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler,
listExtraProvider: ListExtraProvider,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
@@ -36,7 +37,7 @@ class LocalListViewModel @Inject constructor(
settings,
listExtraProvider,
downloadScheduler,
-) {
+), SharedPreferences.OnSharedPreferenceChangeListener {
val onMangaRemoved = MutableEventFlow()
@@ -47,6 +48,18 @@ class LocalListViewModel @Inject constructor(
loadList(filter.snapshot(), append = false).join()
}
}
+ settings.subscribe(this)
+ }
+
+ override fun onCleared() {
+ settings.unsubscribe(this)
+ super.onCleared()
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+ if (key == AppSettings.KEY_LOCAL_MANGA_DIRS) {
+ onRefresh()
+ }
}
fun delete(ids: Set) {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt
index 85eb56d6d..7572b3965 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt
@@ -16,14 +16,13 @@ import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.settings.storage.MangaDirectorySelectDialog
-import java.io.File
+import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
import javax.inject.Inject
@AndroidEntryPoint
class DownloadsSettingsFragment :
BasePreferenceFragment(R.string.downloads),
- SharedPreferences.OnSharedPreferenceChangeListener,
- StorageSelectDialog.OnStorageSelectListener {
+ SharedPreferences.OnSharedPreferenceChangeListener {
@Inject
lateinit var storageManager: LocalStorageManager
@@ -38,6 +37,7 @@ class DownloadsSettingsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
+ findPreference(AppSettings.KEY_LOCAL_MANGA_DIRS)?.bindDirectoriesCount()
settings.subscribe(this)
}
@@ -52,6 +52,10 @@ class DownloadsSettingsFragment :
findPreference(key)?.bindStorageName()
}
+ AppSettings.KEY_LOCAL_MANGA_DIRS -> {
+ findPreference(key)?.bindDirectoriesCount()
+ }
+
AppSettings.KEY_DOWNLOADS_WIFI -> {
updateDownloadsConstraints()
}
@@ -65,14 +69,15 @@ class DownloadsSettingsFragment :
true
}
+ AppSettings.KEY_LOCAL_MANGA_DIRS -> {
+ startActivity(MangaDirectoriesActivity.newIntent(preference.context))
+ true
+ }
+
else -> super.onPreferenceTreeClick(preference)
}
}
- override fun onStorageSelected(file: File) {
- settings.mangaStorageDir = file
- }
-
private fun Preference.bindStorageName() {
viewLifecycleScope.launch {
val storage = storageManager.getDefaultWriteableDir()
@@ -84,6 +89,13 @@ class DownloadsSettingsFragment :
}
}
+ private fun Preference.bindDirectoriesCount() {
+ viewLifecycleScope.launch {
+ val dirs = storageManager.getReadableDirs().size
+ summary = resources.getQuantityString(R.plurals.items, dirs, dirs)
+ }
+ }
+
private fun updateDownloadsConstraints() {
val preference = findPreference(AppSettings.KEY_DOWNLOADS_WIFI)
viewLifecycleScope.launch {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/DirectoryModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/DirectoryModel.kt
index 5113bb2ea..b6226aa03 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/DirectoryModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/DirectoryModel.kt
@@ -9,6 +9,7 @@ class DirectoryModel(
@StringRes val titleRes: Int,
val file: File?,
val isChecked: Boolean,
+ val isAvailable: Boolean,
) : ListModel {
override fun equals(other: Any?): Boolean {
@@ -20,7 +21,8 @@ class DirectoryModel(
if (title != other.title) return false
if (titleRes != other.titleRes) return false
if (file != other.file) return false
- return isChecked == other.isChecked
+ if (isChecked != other.isChecked) return false
+ return isAvailable == other.isAvailable
}
override fun hashCode(): Int {
@@ -28,6 +30,7 @@ class DirectoryModel(
result = 31 * result + titleRes
result = 31 * result + (file?.hashCode() ?: 0)
result = 31 * result + isChecked.hashCode()
+ result = 31 * result + isAvailable.hashCode()
return result
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/MangaDirectorySelectDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/MangaDirectorySelectDialog.kt
index f276ef639..ab9d565d3 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/MangaDirectorySelectDialog.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/MangaDirectorySelectDialog.kt
@@ -36,7 +36,10 @@ class MangaDirectorySelectDialog : AlertDialogFragment()
init {
- launchJob {
- val defaultValue = storageManager.getDefaultWriteableDir()
- val available = storageManager.getWriteableDirs()
- items.value = buildList(available.size + 1) {
- available.mapTo(this) { dir ->
- DirectoryModel(
- title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
- titleRes = 0,
- file = dir,
- isChecked = dir == defaultValue,
- )
- }
- this += DirectoryModel(
- title = null,
- titleRes = R.string.pick_custom_directory,
- file = null,
- isChecked = false,
- )
- }
- }
+ refresh()
}
fun onItemClick(item: DirectoryModel) {
@@ -62,9 +43,36 @@ class MangaDirectorySelectViewModel @Inject constructor(
if (!dir.canWrite()) {
throw AccessDeniedException(dir)
}
- settings.userSpecifiedMangaDirectories += dir
- settings.mangaStorageDir = dir
+ if (dir !in storageManager.getApplicationStorageDirs()) {
+ settings.mangaStorageDir = dir
+ storageManager.setDirIsNoMedia(dir)
+ }
onDismissDialog.call(Unit)
}
}
+
+ fun refresh() {
+ launchJob(Dispatchers.Default) {
+ val defaultValue = storageManager.getDefaultWriteableDir()
+ val available = storageManager.getWriteableDirs()
+ items.value = buildList(available.size + 1) {
+ available.mapTo(this) { dir ->
+ DirectoryModel(
+ title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
+ titleRes = 0,
+ file = dir,
+ isChecked = dir == defaultValue,
+ isAvailable = true,
+ )
+ }
+ this += DirectoryModel(
+ title = null,
+ titleRes = R.string.pick_custom_directory,
+ file = null,
+ isChecked = false,
+ isAvailable = true,
+ )
+ }
+ }
+ }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/DirectoryConfigAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/DirectoryConfigAD.kt
new file mode 100644
index 000000000..e4aa8c5fe
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/DirectoryConfigAD.kt
@@ -0,0 +1,31 @@
+package org.koitharu.kotatsu.settings.storage.directories
+
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.core.util.ext.drawableStart
+import org.koitharu.kotatsu.core.util.ext.textAndVisible
+import org.koitharu.kotatsu.databinding.ItemStorageConfigBinding
+import org.koitharu.kotatsu.settings.storage.DirectoryModel
+
+fun directoryConfigAD(
+ clickListener: OnListItemClickListener,
+) = adapterDelegateViewBinding(
+ { layoutInflater, parent -> ItemStorageConfigBinding.inflate(layoutInflater, parent, false) },
+) {
+
+ binding.imageViewRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) }
+
+ bind {
+ binding.textViewTitle.text = item.title ?: getString(item.titleRes)
+ binding.textViewSubtitle.textAndVisible = item.file?.absolutePath
+ binding.imageViewRemove.isVisible = item.isChecked
+ binding.textViewTitle.drawableStart = if (item.isAvailable) {
+ null
+ } else {
+ ContextCompat.getDrawable(context, R.drawable.ic_alert_outline)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesActivity.kt
new file mode 100644
index 000000000..d4f3283f9
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesActivity.kt
@@ -0,0 +1,93 @@
+package org.koitharu.kotatsu.settings.storage.directories
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.core.graphics.Insets
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
+import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
+import dagger.hilt.android.AndroidEntryPoint
+import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
+import org.koitharu.kotatsu.core.ui.BaseActivity
+import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.core.util.ext.observe
+import org.koitharu.kotatsu.core.util.ext.observeEvent
+import org.koitharu.kotatsu.databinding.ActivityMangaDirectoriesBinding
+import org.koitharu.kotatsu.settings.storage.DirectoryDiffCallback
+import org.koitharu.kotatsu.settings.storage.DirectoryModel
+import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
+
+@AndroidEntryPoint
+class MangaDirectoriesActivity : BaseActivity(),
+ OnListItemClickListener, View.OnClickListener {
+
+ private val viewModel: MangaDirectoriesViewModel by viewModels()
+ private val pickFileTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
+ if (it != null) viewModel.onCustomDirectoryPicked(it)
+ }
+ private val permissionRequestLauncher = registerForActivityResult(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ RequestStorageManagerPermissionContract()
+ } else {
+ ActivityResultContracts.RequestPermission()
+ },
+ ) {
+ if (it) {
+ viewModel.updateList()
+ pickFileTreeLauncher.launch(null)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivityMangaDirectoriesBinding.inflate(layoutInflater))
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ val adapter = AsyncListDifferDelegationAdapter(DirectoryDiffCallback(), directoryConfigAD(this))
+ viewBinding.recyclerView.adapter = adapter
+ viewBinding.fabAdd.setOnClickListener(this)
+ viewModel.items.observe(this) { adapter.items = it }
+ viewModel.isLoading.observe(this) { viewBinding.progressBar.isVisible = it }
+ viewModel.onError.observeEvent(
+ this,
+ SnackbarErrorObserver(viewBinding.root, null, exceptionResolver) {
+ if (it) viewModel.updateList()
+ },
+ )
+ }
+
+ override fun onItemClick(item: DirectoryModel, view: View) {
+ viewModel.onRemoveClick(item.file ?: return)
+ }
+
+ override fun onClick(v: View?) {
+ permissionRequestLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ viewBinding.fabAdd.updateLayoutParams {
+ rightMargin = topMargin + insets.right
+ leftMargin = topMargin + insets.left
+ bottomMargin = topMargin + insets.bottom
+ }
+ viewBinding.root.updatePadding(
+ left = insets.left,
+ right = insets.right,
+ )
+ viewBinding.recyclerView.updatePadding(
+ bottom = insets.bottom,
+ )
+ }
+
+ companion object {
+
+ fun newIntent(context: Context) = Intent(context, MangaDirectoriesActivity::class.java)
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesViewModel.kt
new file mode 100644
index 000000000..6450e59ac
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesViewModel.kt
@@ -0,0 +1,85 @@
+package org.koitharu.kotatsu.settings.storage.directories
+
+import android.net.Uri
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.MutableStateFlow
+import okio.FileNotFoundException
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.ui.BaseViewModel
+import org.koitharu.kotatsu.local.data.LocalStorageManager
+import org.koitharu.kotatsu.settings.storage.DirectoryModel
+import java.io.File
+import javax.inject.Inject
+
+@HiltViewModel
+class MangaDirectoriesViewModel @Inject constructor(
+ private val storageManager: LocalStorageManager,
+ private val settings: AppSettings,
+) : BaseViewModel() {
+
+ val items = MutableStateFlow(emptyList())
+ private var loadingJob: Job? = null
+
+ init {
+ loadList()
+ }
+
+ fun updateList() {
+ loadList()
+ }
+
+ fun onCustomDirectoryPicked(uri: Uri) {
+ launchLoadingJob(Dispatchers.Default) {
+ loadingJob?.cancelAndJoin()
+ storageManager.takePermissions(uri)
+ val dir = storageManager.resolveUri(uri) ?: throw FileNotFoundException()
+ if (!dir.canWrite()) {
+ throw AccessDeniedException(dir)
+ }
+ if (dir !in storageManager.getApplicationStorageDirs()) {
+ settings.userSpecifiedMangaDirectories += dir
+ loadList()
+ }
+ }
+ }
+
+ fun onRemoveClick(directory: File) {
+ settings.userSpecifiedMangaDirectories -= directory
+ if (settings.mangaStorageDir == directory) {
+ settings.mangaStorageDir = null
+ }
+ loadList()
+ }
+
+ private fun loadList() {
+ val prevJob = loadingJob
+ loadingJob = launchJob(Dispatchers.Default) {
+ prevJob?.cancelAndJoin()
+ val applicationDirs = storageManager.getApplicationStorageDirs()
+ val customDirs = settings.userSpecifiedMangaDirectories
+ items.value = buildList(applicationDirs.size + customDirs.size) {
+ applicationDirs.mapTo(this) { dir ->
+ DirectoryModel(
+ title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
+ titleRes = 0,
+ file = dir,
+ isChecked = false,
+ isAvailable = dir.canRead() && dir.canWrite(),
+ )
+ }
+ customDirs.mapTo(this) { dir ->
+ DirectoryModel(
+ title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
+ titleRes = 0,
+ file = dir,
+ isChecked = true,
+ isAvailable = dir.canRead() && dir.canWrite(),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_categories.xml b/app/src/main/res/layout/activity_categories.xml
index 07ab6871f..97799592d 100644
--- a/app/src/main/res/layout/activity_categories.xml
+++ b/app/src/main/res/layout/activity_categories.xml
@@ -58,7 +58,6 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_new_category"
- android:src="@drawable/ic_add"
android:text="@string/create_category"
app:fabSize="normal"
app:icon="@drawable/ic_add"
diff --git a/app/src/main/res/layout/activity_manga_directories.xml b/app/src/main/res/layout/activity_manga_directories.xml
new file mode 100644
index 000000000..5e8ffdf4b
--- /dev/null
+++ b/app/src/main/res/layout/activity_manga_directories.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_storage_config.xml b/app/src/main/res/layout/item_storage_config.xml
new file mode 100644
index 000000000..e1a552325
--- /dev/null
+++ b/app/src/main/res/layout/item_storage_config.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/opt_local.xml b/app/src/main/res/menu/opt_local.xml
index 517cb28f9..af3cce235 100644
--- a/app/src/main/res/menu/opt_local.xml
+++ b/app/src/main/res/menu/opt_local.xml
@@ -9,4 +9,10 @@
android:title="@string/_import"
app:showAsAction="never" />
-
\ No newline at end of file
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0c95c0b16..90f39ddb6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -448,4 +448,5 @@
Custom directory
Pick custom directory
You have no access to this file or directory
+ Local manga directories
diff --git a/app/src/main/res/xml/pref_downloads.xml b/app/src/main/res/xml/pref_downloads.xml
index 82554d8b9..c08522ac4 100644
--- a/app/src/main/res/xml/pref_downloads.xml
+++ b/app/src/main/res/xml/pref_downloads.xml
@@ -1,6 +1,12 @@
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+
+ android:title="@string/downloads_wifi_only"
+ app:allowDividerAbove="true" />