Improve local manga directories config screen

devel
Koitharu 6 months ago
parent d0ed1fb85f
commit 0d5229b112
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -21,8 +21,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 23 minSdk = 23
targetSdk = 36 targetSdk = 36
versionCode = 1031 versionCode = 1032
versionName = '9.3' versionName = '9.4'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {

@ -5,7 +5,10 @@ import android.view.View
import androidx.annotation.Px import androidx.annotation.Px
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() { class SpacingItemDecoration(
@Px private val spacing: Int,
private val withBottomPadding: Boolean,
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets( override fun getItemOffsets(
outRect: Rect, outRect: Rect,
@ -13,6 +16,6 @@ class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDec
parent: RecyclerView, parent: RecyclerView,
state: RecyclerView.State, state: RecyclerView.State,
) { ) {
outRect.set(spacing, spacing, spacing, spacing) outRect.set(spacing, spacing, spacing, if (withBottomPadding) spacing else 0)
} }
} }

@ -13,11 +13,11 @@ import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import dagger.Reusable import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Cache import okhttp3.Cache
import org.koitharu.kotatsu.core.LocalizedAppContext
import org.koitharu.kotatsu.core.exceptions.NonFileUriException import org.koitharu.kotatsu.core.exceptions.NonFileUriException
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.computeSize
@ -39,8 +39,8 @@ private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB
@Reusable @Reusable
class LocalStorageManager @Inject constructor( class LocalStorageManager @Inject constructor(
@ApplicationContext private val context: Context, @LocalizedAppContext private val context: Context,
private val settings: AppSettings, private val settings: AppSettings,
) { ) {
val contentResolver: ContentResolver val contentResolver: ContentResolver

@ -51,6 +51,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
binding.chipsType.onChipClickListener = this binding.chipsType.onChipClickListener = this
binding.chipBackup.setOnClickListener(this) binding.chipBackup.setOnClickListener(this)
binding.chipSync.setOnClickListener(this) binding.chipSync.setOnClickListener(this)
binding.chipDirectories.setOnClickListener(this)
viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged) viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged)
viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged) viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged)
@ -86,6 +87,10 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
val accountType = getString(R.string.account_type_sync) val accountType = getString(R.string.account_type_sync)
am.addAccount(accountType, accountType, null, null, requireActivity(), null, null) am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
} }
R.id.chip_directories -> {
router.openDirectoriesSettings()
}
} }
} }

@ -37,7 +37,7 @@ fun searchResultsAD(
binding.recyclerView.addItemDecoration(selectionDecoration) binding.recyclerView.addItemDecoration(selectionDecoration)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer) val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing)) binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing, withBottomPadding = true))
val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener) val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener)
binding.buttonMore.setOnClickListener(eventListener) binding.buttonMore.setOnClickListener(eventListener)

@ -29,7 +29,7 @@ fun searchSuggestionMangaListAD(
left = recyclerView.paddingLeft - spacing, left = recyclerView.paddingLeft - spacing,
right = recyclerView.paddingRight - spacing, right = recyclerView.paddingRight - spacing,
) )
recyclerView.addItemDecoration(SpacingItemDecoration(spacing)) recyclerView.addItemDecoration(SpacingItemDecoration(spacing, withBottomPadding = true))
val scrollResetCallback = RecyclerViewScrollCallback(recyclerView, 0, 0) val scrollResetCallback = RecyclerViewScrollCallback(recyclerView, 0, 0)
bind { bind {

@ -1,38 +1,66 @@
package org.koitharu.kotatsu.settings.storage.directories package org.koitharu.kotatsu.settings.storage.directories
import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.isGone
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemStorageConfigBinding import org.koitharu.kotatsu.databinding.ItemStorageConfig2Binding
import org.koitharu.kotatsu.settings.storage.DirectoryModel
fun directoryConfigAD( fun directoryConfigAD(
clickListener: OnListItemClickListener<DirectoryModel>, clickListener: OnListItemClickListener<DirectoryConfigModel>,
) = adapterDelegateViewBinding<DirectoryModel, DirectoryModel, ItemStorageConfigBinding>( ) = adapterDelegateViewBinding<DirectoryConfigModel, DirectoryConfigModel, ItemStorageConfig2Binding>(
{ layoutInflater, parent -> ItemStorageConfigBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent -> ItemStorageConfig2Binding.inflate(layoutInflater, parent, false) },
) { ) {
binding.buttonRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) } binding.buttonRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) }
binding.buttonRemove.setTooltipCompat(binding.buttonRemove.contentDescription) binding.buttonRemove.setTooltipCompat(binding.buttonRemove.contentDescription)
bind { bind {
binding.textViewTitle.text = item.title ?: getString(item.titleRes) binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.file?.absolutePath binding.textViewSubtitle.text = item.path.absolutePath
binding.buttonRemove.isVisible = item.isRemovable binding.buttonRemove.isGone = item.isAppPrivate
binding.buttonRemove.isEnabled = !item.isChecked binding.buttonRemove.isEnabled = !item.isDefault
binding.textViewTitle.drawableStart = if (!item.isAvailable) { binding.spacer.visibility = if (item.isAppPrivate) {
ContextCompat.getDrawable(context, R.drawable.ic_alert_outline)?.apply { View.INVISIBLE
setTint(ContextCompat.getColor(context, R.color.warning)) } else {
} View.GONE
} else if (item.isChecked) { }
ContextCompat.getDrawable(context, R.drawable.ic_download) binding.textViewInfo.textAndVisible = buildSpannedString {
} else { if (item.isDefault) {
null bold {
} append(getString(R.string.download_default_directory))
} }
}
if (!item.isAccessible) {
if (isNotEmpty()) appendLine()
color(
context.getThemeColor(
androidx.appcompat.R.attr.colorError,
ContextCompat.getColor(context, R.color.common_red),
),
) {
append(getString(R.string.no_write_permission_to_file))
}
}
if (item.isAppPrivate) {
if (isNotEmpty()) appendLine()
append(getString(R.string.private_app_directory_warning))
}
}
binding.indicatorSize.max = FileSize.BYTES.convert(item.available, FileSize.KILOBYTES).toInt()
binding.indicatorSize.progress = FileSize.BYTES.convert(item.size, FileSize.KILOBYTES).toInt()
binding.textViewSize.text = context.getString(
R.string.available_pattern,
FileSize.BYTES.format(context, item.available),
)
}
} }

@ -0,0 +1,22 @@
package org.koitharu.kotatsu.settings.storage.directories
import androidx.recyclerview.widget.DiffUtil.ItemCallback
class DirectoryConfigDiffCallback : ItemCallback<DirectoryConfigModel>() {
override fun areItemsTheSame(oldItem: DirectoryConfigModel, newItem: DirectoryConfigModel): Boolean {
return oldItem.path == newItem.path
}
override fun areContentsTheSame(oldItem: DirectoryConfigModel, newItem: DirectoryConfigModel): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: DirectoryConfigModel, newItem: DirectoryConfigModel): Any? {
return if (oldItem.isDefault != newItem.isDefault) {
Unit
} else {
super.getChangePayload(oldItem, newItem)
}
}
}

@ -0,0 +1,19 @@
package org.koitharu.kotatsu.settings.storage.directories
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.io.File
data class DirectoryConfigModel(
val title: String,
val path: File,
val isDefault: Boolean,
val isAppPrivate: Boolean,
val isAccessible: Boolean,
val size: Long,
val available: Long,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is DirectoryConfigModel && path == other.path
}
}

@ -20,18 +20,17 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.ActivityMangaDirectoriesBinding 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 import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
@AndroidEntryPoint @AndroidEntryPoint
class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>(), class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>(),
OnListItemClickListener<DirectoryModel>, View.OnClickListener { OnListItemClickListener<DirectoryConfigModel>, View.OnClickListener {
private val viewModel: MangaDirectoriesViewModel by viewModels() private val viewModel: MangaDirectoriesViewModel by viewModels()
private val pickFileTreeLauncher = OpenDocumentTreeHelper( private val pickFileTreeLauncher = OpenDocumentTreeHelper(
@ -63,8 +62,10 @@ class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityMangaDirectoriesBinding.inflate(layoutInflater)) setContentView(ActivityMangaDirectoriesBinding.inflate(layoutInflater))
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false) setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
val adapter = AsyncListDifferDelegationAdapter(DirectoryDiffCallback(), directoryConfigAD(this)) val adapter = AsyncListDifferDelegationAdapter(DirectoryConfigDiffCallback(), directoryConfigAD(this))
viewBinding.recyclerView.adapter = adapter val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing_large)
viewBinding.recyclerView.adapter = adapter
viewBinding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing, withBottomPadding = false))
viewBinding.fabAdd.setOnClickListener(this) viewBinding.fabAdd.setOnClickListener(this)
viewModel.items.observe(this) { adapter.items = it } viewModel.items.observe(this) { adapter.items = it }
viewModel.isLoading.observe(this) { viewBinding.progressBar.isVisible = it } viewModel.isLoading.observe(this) { viewBinding.progressBar.isVisible = it }
@ -76,8 +77,8 @@ class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>()
) )
} }
override fun onItemClick(item: DirectoryModel, view: View) { override fun onItemClick(item: DirectoryConfigModel, view: View) {
viewModel.onRemoveClick(item.file ?: return) viewModel.onRemoveClick(item.path)
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.settings.storage.directories package org.koitharu.kotatsu.settings.storage.directories
import android.net.Uri import android.net.Uri
import android.os.StatFs
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -8,82 +9,87 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.isReadable import org.koitharu.kotatsu.core.util.ext.isReadable
import org.koitharu.kotatsu.core.util.ext.isWriteable import org.koitharu.kotatsu.core.util.ext.isWriteable
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MangaDirectoriesViewModel @Inject constructor( class MangaDirectoriesViewModel @Inject constructor(
private val storageManager: LocalStorageManager, private val storageManager: LocalStorageManager,
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
val items = MutableStateFlow(emptyList<DirectoryModel>()) val items = MutableStateFlow(emptyList<DirectoryConfigModel>())
private var loadingJob: Job? = null private var loadingJob: Job? = null
init { init {
loadList() loadList()
} }
fun updateList() { fun updateList() {
loadList() loadList()
} }
fun onCustomDirectoryPicked(uri: Uri) { fun onCustomDirectoryPicked(uri: Uri) {
launchLoadingJob(Dispatchers.Default) { launchLoadingJob(Dispatchers.Default) {
loadingJob?.cancelAndJoin() loadingJob?.cancelAndJoin()
storageManager.takePermissions(uri) storageManager.takePermissions(uri)
val dir = storageManager.resolveUri(uri) val dir = storageManager.resolveUri(uri)
if (!dir.canRead()) { if (!dir.canRead()) {
throw AccessDeniedException(dir) throw AccessDeniedException(dir)
} }
if (dir !in storageManager.getApplicationStorageDirs()) { if (dir !in storageManager.getApplicationStorageDirs()) {
settings.userSpecifiedMangaDirectories += dir settings.userSpecifiedMangaDirectories += dir
loadList() loadList()
} }
} }
} }
fun onRemoveClick(directory: File) { fun onRemoveClick(directory: File) {
settings.userSpecifiedMangaDirectories -= directory settings.userSpecifiedMangaDirectories -= directory
if (settings.mangaStorageDir == directory) { if (settings.mangaStorageDir == directory) {
settings.mangaStorageDir = null settings.mangaStorageDir = null
} }
loadList() loadList()
} }
private fun loadList() { private fun loadList() {
val prevJob = loadingJob val prevJob = loadingJob
loadingJob = launchJob(Dispatchers.Default) { loadingJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin() prevJob?.cancelAndJoin()
val downloadDir = storageManager.getDefaultWriteableDir() val downloadDir = storageManager.getDefaultWriteableDir()
val applicationDirs = storageManager.getApplicationStorageDirs() val applicationDirs = storageManager.getApplicationStorageDirs()
val customDirs = settings.userSpecifiedMangaDirectories - applicationDirs val customDirs = settings.userSpecifiedMangaDirectories - applicationDirs
items.value = buildList(applicationDirs.size + customDirs.size) { items.value = buildList(applicationDirs.size + customDirs.size) {
applicationDirs.mapTo(this) { dir -> applicationDirs.mapTo(this) { dir ->
DirectoryModel( dir.toDirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false), isDefault = dir == downloadDir,
titleRes = 0, isAppPrivate = true,
file = dir, )
isChecked = dir == downloadDir, }
isAvailable = dir.isReadable() && dir.isWriteable(), customDirs.mapTo(this) { dir ->
isRemovable = false, dir.toDirectoryModel(
) isDefault = dir == downloadDir,
} isAppPrivate = false,
customDirs.mapTo(this) { dir -> )
DirectoryModel( }
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false), }
titleRes = 0, }
file = dir, }
isChecked = dir == downloadDir,
isAvailable = dir.isReadable() && dir.isWriteable(), private suspend fun File.toDirectoryModel(
isRemovable = true, isDefault: Boolean,
) isAppPrivate: Boolean,
} ) = DirectoryConfigModel(
} title = storageManager.getDirectoryDisplayName(this, isFullPath = false),
} path = this,
} isDefault = isDefault,
isAccessible = isReadable() && isWriteable(),
isAppPrivate = isAppPrivate,
size = computeSize(),
available = StatFs(this.absolutePath).availableBytes,
)
} }

@ -1,57 +1,60 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
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" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"> android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar" <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:id="@+id/appbar"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:fitsSystemWindows="true"> android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar" <com.google.android.material.appbar.MaterialToolbar
android:layout_width="match_parent" android:id="@id/toolbar"
android:layout_height="?attr/actionBarSize" android:layout_width="match_parent"
app:layout_scrollFlags="noScroll"> android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="noScroll">
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" <androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent" android:id="@+id/recyclerView"
android:layout_height="match_parent" android:layout_width="match_parent"
android:clipToPadding="false" android:layout_height="match_parent"
android:orientation="vertical" android:clipToPadding="false"
android:scrollbars="vertical" android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:paddingBottom="@dimen/list_spacing_large"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
<com.google.android.material.progressindicator.LinearProgressIndicator app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:id="@+id/progressBar" tools:listitem="@layout/item_storage_config2" />
android:layout_width="match_parent"
android:layout_height="wrap_content" <com.google.android.material.progressindicator.LinearProgressIndicator
android:indeterminate="true" android:id="@+id/progressBar"
android:visibility="gone" android:layout_width="match_parent"
app:layout_anchor="@id/appbar" android:layout_height="wrap_content"
app:layout_anchorGravity="bottom" /> android:indeterminate="true"
android:visibility="gone"
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton app:layout_anchor="@id/appbar"
android:id="@+id/fab_add" app:layout_anchorGravity="bottom" />
android:layout_width="wrap_content"
android:layout_height="wrap_content" <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:layout_margin="16dp" android:id="@+id/fab_add"
android:contentDescription="@string/pick_custom_directory" android:layout_width="wrap_content"
android:text="@string/add" android:layout_height="wrap_content"
app:fabSize="normal" android:layout_margin="16dp"
app:icon="@drawable/ic_add" android:contentDescription="@string/pick_custom_directory"
app:layout_anchor="@id/recyclerView" android:text="@string/add"
app:layout_anchorGravity="bottom|end" app:fabSize="normal"
app:layout_behavior="org.koitharu.kotatsu.core.ui.util.ShrinkOnScrollBehavior" app:icon="@drawable/ic_add"
app:layout_dodgeInsetEdges="bottom" /> 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> </androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
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"
tools:layout_margin="@dimen/screen_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/margin_small">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/screen_padding"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleMedium"
tools:text="@tools:sample/lorem[3]" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[5]" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/screen_padding"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/textView_size"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_small"
android:layout_weight="1"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:textColorSecondary"
tools:text="250MB / 10GB" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/indicator_size"
android:layout_width="160dp"
android:layout_height="wrap_content"
app:trackCornerRadius="5dp"
app:trackThickness="10dp"
tools:progress="40" />
</LinearLayout>
<TextView
android:id="@+id/textView_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?android:textColorSecondary"
tools:text="Content will be removed within application" />
<Button
android:id="@+id/button_remove"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="@dimen/margin_small"
android:text="@string/remove" />
<View
android:id="@+id/spacer"
android:layout_width="match_parent"
android:layout_height="@dimen/margin_small"
tools:visibility="gone" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

@ -64,6 +64,14 @@
android:text="@string/sync_auth" android:text="@string/sync_auth"
app:chipIcon="@drawable/ic_sync" /> app:chipIcon="@drawable/ic_sync" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_directories"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/local_manga_directories"
app:chipIcon="@drawable/ic_storage" />
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
</HorizontalScrollView> </HorizontalScrollView>

@ -900,4 +900,7 @@
<string name="data_removal">Data removal</string> <string name="data_removal">Data removal</string>
<string name="privacy">Privacy</string> <string name="privacy">Privacy</string>
<string name="source_broken_warning">This manga source has been marked as broken. Some features may not work</string> <string name="source_broken_warning">This manga source has been marked as broken. Some features may not work</string>
<string name="download_default_directory">Default directory for downloading manga</string>
<string name="private_app_directory_warning">This directory with all data will be deleted if you uninstall the application</string>
<string name="available_pattern">%1$s available</string>
</resources> </resources>

Loading…
Cancel
Save