Configure manga directories
parent
feca7ba3fc
commit
2bcba1eb21
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
Loading…
Reference in New Issue