Improve local manga directories config screen

This commit is contained in:
Koitharu
2025-10-26 16:33:59 +02:00
parent d0ed1fb85f
commit 0d5229b112
15 changed files with 340 additions and 149 deletions

View File

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

View File

@@ -5,7 +5,10 @@ import android.view.View
import androidx.annotation.Px
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(
outRect: Rect,
@@ -13,6 +16,6 @@ class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDec
parent: RecyclerView,
state: RecyclerView.State,
) {
outRect.set(spacing, spacing, spacing, spacing)
outRect.set(spacing, spacing, spacing, if (withBottomPadding) spacing else 0)
}
}

View File

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

View File

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

View File

@@ -37,7 +37,7 @@ fun searchResultsAD(
binding.recyclerView.addItemDecoration(selectionDecoration)
binding.recyclerView.adapter = adapter
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)
binding.buttonMore.setOnClickListener(eventListener)

View File

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

View File

@@ -1,38 +1,66 @@
package org.koitharu.kotatsu.settings.storage.directories
import android.view.View
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 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.FileSize
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemStorageConfigBinding
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import org.koitharu.kotatsu.databinding.ItemStorageConfig2Binding
fun directoryConfigAD(
clickListener: OnListItemClickListener<DirectoryModel>,
) = adapterDelegateViewBinding<DirectoryModel, DirectoryModel, ItemStorageConfigBinding>(
{ layoutInflater, parent -> ItemStorageConfigBinding.inflate(layoutInflater, parent, false) },
clickListener: OnListItemClickListener<DirectoryConfigModel>,
) = adapterDelegateViewBinding<DirectoryConfigModel, DirectoryConfigModel, ItemStorageConfig2Binding>(
{ layoutInflater, parent -> ItemStorageConfig2Binding.inflate(layoutInflater, parent, false) },
) {
binding.buttonRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) }
binding.buttonRemove.setTooltipCompat(binding.buttonRemove.contentDescription)
binding.buttonRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) }
binding.buttonRemove.setTooltipCompat(binding.buttonRemove.contentDescription)
bind {
binding.textViewTitle.text = item.title ?: getString(item.titleRes)
binding.textViewSubtitle.textAndVisible = item.file?.absolutePath
binding.buttonRemove.isVisible = item.isRemovable
binding.buttonRemove.isEnabled = !item.isChecked
binding.textViewTitle.drawableStart = if (!item.isAvailable) {
ContextCompat.getDrawable(context, R.drawable.ic_alert_outline)?.apply {
setTint(ContextCompat.getColor(context, R.color.warning))
}
} else if (item.isChecked) {
ContextCompat.getDrawable(context, R.drawable.ic_download)
} else {
null
}
}
bind {
binding.textViewTitle.text = item.title
binding.textViewSubtitle.text = item.path.absolutePath
binding.buttonRemove.isGone = item.isAppPrivate
binding.buttonRemove.isEnabled = !item.isDefault
binding.spacer.visibility = if (item.isAppPrivate) {
View.INVISIBLE
} else {
View.GONE
}
binding.textViewInfo.textAndVisible = buildSpannedString {
if (item.isDefault) {
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),
)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

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

View File

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

View File

@@ -1,57 +1,60 @@
<?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">
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="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.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
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.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
</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" />
<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:paddingBottom="@dimen/list_spacing_large"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_storage_config2" />
<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.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" />
<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>

View File

@@ -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>

View File

@@ -64,6 +64,14 @@
android:text="@string/sync_auth"
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>
</HorizontalScrollView>

View File

@@ -900,4 +900,7 @@
<string name="data_removal">Data removal</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="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>