Configure manga directories
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
<data android:host="sync-settings" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity"
|
||||
android:label="@string/local_manga_directories" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
|
||||
@@ -249,11 +249,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
var mangaStorageDir: File?
|
||||
get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
|
||||
File(it)
|
||||
}?.takeIf { it.exists() }
|
||||
}?.takeIf { it.exists() && it in userSpecifiedMangaDirectories }
|
||||
set(value) = prefs.edit {
|
||||
if (value == null) {
|
||||
remove(KEY_LOCAL_STORAGE)
|
||||
} else {
|
||||
val userDirs = userSpecifiedMangaDirectories
|
||||
if (value !in userDirs) {
|
||||
userSpecifiedMangaDirectories = userDirs + value
|
||||
}
|
||||
putString(KEY_LOCAL_STORAGE, value.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +78,6 @@ abstract class MangaListFragment :
|
||||
private val spanSizeLookup = SpanSizeLookup()
|
||||
private val listCommitCallback = Runnable {
|
||||
spanSizeLookup.invalidateCache()
|
||||
viewBinding?.let {
|
||||
paginationListener?.onScrolled(it.recyclerView, 0, 0)
|
||||
}
|
||||
}
|
||||
open val isSwipeRefreshEnabled = true
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DIR_NAME = "manga"
|
||||
private const val NOMEDIA = ".nomedia"
|
||||
private const val CACHE_DISK_PERCENTAGE = 0.02
|
||||
private const val CACHE_SIZE_MIN: Long = 10 * 1024 * 1024 // 10MB
|
||||
private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB
|
||||
@@ -77,18 +78,23 @@ class LocalStorageManager @Inject constructor(
|
||||
preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() }
|
||||
}
|
||||
|
||||
suspend fun getApplicationStorageDirs(): Set<File> = 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<File> {
|
||||
val set = getAvailableStorageDirs()
|
||||
set.addAll(settings.userSpecifiedMangaDirectories)
|
||||
settings.mangaStorageDir?.let {
|
||||
set.add(it)
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Unit>,
|
||||
) : MenuProvider {
|
||||
|
||||
@@ -20,6 +23,12 @@ class LocalListMenuProvider(
|
||||
onImportClick()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_settings -> {
|
||||
context.startActivity(MangaDirectoriesActivity.newIntent(context))
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Unit>()
|
||||
|
||||
@@ -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<Long>) {
|
||||
|
||||
@@ -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<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
|
||||
findPreference<Preference>(AppSettings.KEY_LOCAL_MANGA_DIRS)?.bindDirectoriesCount()
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ class DownloadsSettingsFragment :
|
||||
findPreference<Preference>(key)?.bindStorageName()
|
||||
}
|
||||
|
||||
AppSettings.KEY_LOCAL_MANGA_DIRS -> {
|
||||
findPreference<Preference>(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<Preference>(AppSettings.KEY_DOWNLOADS_WIFI)
|
||||
viewLifecycleScope.launch {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ class MangaDirectorySelectDialog : AlertDialogFragment<DialogDirectorySelectBind
|
||||
ActivityResultContracts.RequestPermission()
|
||||
},
|
||||
) {
|
||||
pickFileTreeLauncher.launch(null)
|
||||
if (it) {
|
||||
viewModel.refresh()
|
||||
pickFileTreeLauncher.launch(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogDirectorySelectBinding {
|
||||
|
||||
@@ -24,26 +24,7 @@ class MangaDirectorySelectViewModel @Inject constructor(
|
||||
val onPickDirectory = MutableEventFlow<Unit>()
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DirectoryModel>,
|
||||
) = adapterDelegateViewBinding<DirectoryModel, DirectoryModel, ItemStorageConfigBinding>(
|
||||
{ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActivityMangaDirectoriesBinding>(),
|
||||
OnListItemClickListener<DirectoryModel>, 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<ViewGroup.MarginLayoutParams> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<DirectoryModel>())
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
57
app/src/main/res/layout/activity_manga_directories.xml
Normal file
57
app/src/main/res/layout/activity_manga_directories.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="noScroll">
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_anchor="@id/appbar"
|
||||
app:layout_anchorGravity="bottom" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fab_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/pick_custom_directory"
|
||||
android:text="@string/add"
|
||||
app:fabSize="normal"
|
||||
app:icon="@drawable/ic_add"
|
||||
app:layout_anchor="@id/recyclerView"
|
||||
app:layout_anchorGravity="bottom|end"
|
||||
app:layout_behavior="org.koitharu.kotatsu.core.ui.util.ShrinkOnScrollBehavior"
|
||||
app:layout_dodgeInsetEdges="bottom" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
53
app/src/main/res/layout/item_storage_config.xml
Normal file
53
app/src/main/res/layout/item_storage_config.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?listPreferredItemHeight"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="12dp"
|
||||
android:paddingStart="?listPreferredItemPaddingStart">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="6dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
app:drawableTint="@color/warning"
|
||||
tools:drawableStart="@drawable/ic_alert_outline"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
tools:text="@tools:sample/lorem[20]" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView_remove"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/remove"
|
||||
android:padding="?listPreferredItemPaddingEnd"
|
||||
app:srcCompat="@drawable/ic_delete" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -9,4 +9,10 @@
|
||||
android:title="@string/_import"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/settings"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
@@ -448,4 +448,5 @@
|
||||
<string name="custom_directory">Custom directory</string>
|
||||
<string name="pick_custom_directory">Pick custom directory</string>
|
||||
<string name="no_access_to_file">You have no access to this file or directory</string>
|
||||
<string name="local_manga_directories">Local manga directories</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
android:key="local_manga_dirs"
|
||||
android:persistent="false"
|
||||
android:title="@string/local_manga_directories" />
|
||||
|
||||
<Preference
|
||||
android:key="local_storage"
|
||||
@@ -11,7 +17,8 @@
|
||||
android:defaultValue="false"
|
||||
android:key="downloads_wifi"
|
||||
android:summary="@string/downloads_wifi_only_summary"
|
||||
android:title="@string/downloads_wifi_only" />
|
||||
android:title="@string/downloads_wifi_only"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
|
||||
Reference in New Issue
Block a user