Action to remove read local chapters
This commit is contained in:
@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.find
|
||||
import org.koitharu.kotatsu.parsers.util.isNumeric
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||
@@ -508,6 +507,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
|
||||
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
|
||||
const val KEY_COOKIES_CLEAR = "cookies_clear"
|
||||
const val KEY_CHAPTERS_CLEAR = "chapters_clear"
|
||||
const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear"
|
||||
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
|
||||
const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear"
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
@@ -169,3 +170,11 @@ fun MaterialButton.setProgressIcon() {
|
||||
icon = progressDrawable
|
||||
progressDrawable.start()
|
||||
}
|
||||
|
||||
fun Chip.setProgressIcon() {
|
||||
val progressDrawable = CircularProgressDrawable(context)
|
||||
progressDrawable.strokeWidth = resources.resolveDp(2f)
|
||||
progressDrawable.setColorSchemeColors(currentTextColor)
|
||||
chipIcon = progressDrawable
|
||||
progressDrawable.start()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.koitharu.kotatsu.local.domain
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.fold
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.core.model.findById
|
||||
import org.koitharu.kotatsu.core.model.ids
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeleteReadChaptersUseCase @Inject constructor(
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(manga: Manga): Int {
|
||||
val localManga = if (manga.isLocal) {
|
||||
LocalManga(manga)
|
||||
} else {
|
||||
checkNotNull(localMangaRepository.findSavedManga(manga)) { "Cannot find local manga" }
|
||||
}
|
||||
val task = getDeletionTask(localManga) ?: return 0
|
||||
localMangaRepository.deleteChapters(task.manga.manga, task.chaptersIds)
|
||||
emitUpdate(localManga)
|
||||
return task.chaptersIds.size
|
||||
}
|
||||
|
||||
suspend operator fun invoke(): Int {
|
||||
val list = localMangaRepository.getList(0, null)
|
||||
if (list.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
return channelFlow {
|
||||
for (manga in list) {
|
||||
launch(Dispatchers.Default) {
|
||||
val task = runCatchingCancellable {
|
||||
getDeletionTask(LocalManga(manga))
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
if (task != null) {
|
||||
send(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.buffer().map {
|
||||
runCatchingCancellable {
|
||||
localMangaRepository.deleteChapters(it.manga.manga, it.chaptersIds)
|
||||
emitUpdate(it.manga)
|
||||
it.chaptersIds.size
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrDefault(0)
|
||||
}.fold(0) { acc, x -> acc + x }
|
||||
}
|
||||
|
||||
private suspend fun getDeletionTask(manga: LocalManga): DeletionTask? {
|
||||
val history = historyRepository.getOne(manga.manga) ?: return null
|
||||
val chapters = manga.manga.chapters ?: localMangaRepository.getDetails(manga.manga).chapters
|
||||
if (chapters.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
val branch = (chapters.findById(history.chapterId) ?: return null).branch
|
||||
val filteredChapters = manga.manga.getChapters(branch)?.takeWhile { it.id != history.chapterId }
|
||||
return if (filteredChapters.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
DeletionTask(
|
||||
manga = manga,
|
||||
chaptersIds = filteredChapters.ids(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun emitUpdate(subject: LocalManga) {
|
||||
val updated = localMangaRepository.getDetails(subject.manga)
|
||||
localStorageChanges.emit(subject.copy(manga = updated))
|
||||
}
|
||||
|
||||
private class DeletionTask(
|
||||
val manga: LocalManga,
|
||||
val chaptersIds: Set<Long>,
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.local.ui.info
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -14,9 +16,11 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.setProgressIcon
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding
|
||||
@@ -24,14 +28,12 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() {
|
||||
class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>(), View.OnClickListener {
|
||||
|
||||
private val viewModel: LocalInfoViewModel by viewModels()
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
return super.onBuildDialog(builder)
|
||||
.setTitle(R.string.saved_manga)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
return super.onBuildDialog(builder).setTitle(R.string.saved_manga).setNegativeButton(R.string.close, null)
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogLocalInfoBinding {
|
||||
@@ -43,13 +45,44 @@ class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() {
|
||||
viewModel.path.observe(this) {
|
||||
binding.textViewPath.text = it
|
||||
}
|
||||
combine(viewModel.size, viewModel.availableSize, ::Pair).observe(this) {
|
||||
binding.chipCleanup.setOnClickListener(this)
|
||||
combine(viewModel.size, viewModel.availableSize, ::Pair).observe(viewLifecycleOwner) {
|
||||
if (it.first >= 0 && it.second >= 0) {
|
||||
setSegments(it.first, it.second)
|
||||
} else {
|
||||
binding.barView.animateSegments(emptyList())
|
||||
}
|
||||
}
|
||||
viewModel.onCleanedUp.observeEvent(viewLifecycleOwner, ::onCleanedUp)
|
||||
viewModel.isCleaningUp.observe(viewLifecycleOwner) { loading ->
|
||||
binding.chipCleanup.isClickable = !loading
|
||||
dialog?.setCancelable(!loading)
|
||||
if (loading) {
|
||||
binding.chipCleanup.setProgressIcon()
|
||||
} else {
|
||||
binding.chipCleanup.setChipIconResource(R.drawable.ic_delete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.chip_cleanup -> viewModel.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCleanedUp(result: Pair<Int, Long>) {
|
||||
val c = context ?: return
|
||||
val text = if (result.first == 0 && result.second == 0L) {
|
||||
c.getString(R.string.no_chapters_deleted)
|
||||
} else {
|
||||
c.getString(
|
||||
R.string.chapters_deleted_pattern,
|
||||
c.resources.getQuantityString(R.plurals.chapters, result.first, result.first),
|
||||
FileSize.BYTES.format(c, result.second),
|
||||
)
|
||||
}
|
||||
Toast.makeText(c, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun setSegments(size: Long, available: Long) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.local.ui.info
|
||||
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -8,12 +7,14 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.computeSize
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.local.domain.DeleteReadChaptersUseCase
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -21,21 +22,42 @@ class LocalInfoViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val storageManager: LocalStorageManager,
|
||||
private val deleteReadChaptersUseCase: DeleteReadChaptersUseCase,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val manga = savedStateHandle.require<ParcelableManga>(LocalInfoDialog.ARG_MANGA).manga
|
||||
|
||||
val isCleaningUp = MutableStateFlow(false)
|
||||
val onCleanedUp = MutableEventFlow<Pair<Int, Long>>()
|
||||
|
||||
val path = MutableStateFlow<String?>(null)
|
||||
val size = MutableStateFlow(-1L)
|
||||
val availableSize = MutableStateFlow(-1L)
|
||||
|
||||
init {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val file = manga.url.toUri().toFileOrNull() ?: localMangaRepository.findSavedManga(manga)?.file
|
||||
requireNotNull(file)
|
||||
path.value = file.path
|
||||
size.value = file.computeSize()
|
||||
availableSize.value = storageManager.computeAvailableSize()
|
||||
computeSize()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
isCleaningUp.value = true
|
||||
val oldSize = size.value
|
||||
val chaptersCount = deleteReadChaptersUseCase.invoke(manga)
|
||||
computeSize().join()
|
||||
val newSize = size.value
|
||||
onCleanedUp.call(chaptersCount to oldSize - newSize)
|
||||
} finally {
|
||||
isCleaningUp.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeSize() = launchLoadingJob(Dispatchers.Default) {
|
||||
val file = manga.url.toUri().toFileOrNull() ?: localMangaRepository.findSavedManga(manga)?.file
|
||||
requireNotNull(file)
|
||||
path.value = file.path
|
||||
size.value = file.computeSize()
|
||||
availableSize.value = storageManager.computeAvailableSize()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
||||
}
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView))
|
||||
viewModel.onChaptersCleanedUp.observeEvent(viewLifecycleOwner, ::onChaptersCleanedUp)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
@@ -129,6 +130,11 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_CHAPTERS_CLEAR -> {
|
||||
cleanupChapters()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
||||
viewModel.clearUpdatesFeed()
|
||||
true
|
||||
@@ -191,6 +197,21 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChaptersCleanedUp(result: Pair<Int, Long>) {
|
||||
val c = context ?: return
|
||||
val text = if (result.first == 0 && result.second == 0L) {
|
||||
c.getString(R.string.no_chapters_deleted)
|
||||
} else {
|
||||
c.getString(
|
||||
R.string.chapters_deleted_pattern,
|
||||
c.resources.getQuantityString(R.plurals.chapters, result.first, result.first),
|
||||
FileSize.BYTES.format(c, result.second),
|
||||
)
|
||||
}
|
||||
Snackbar.make(listView, text, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
|
||||
private fun Preference.bindBytesSizeSummary(stateFlow: StateFlow<Long>) {
|
||||
stateFlow.observe(viewLifecycleOwner) { size ->
|
||||
summary = if (size < 0) {
|
||||
@@ -235,6 +256,16 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun cleanupChapters() {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.delete_read_chapters)
|
||||
.setMessage(R.string.delete_read_chapters_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
viewModel.cleanupChapters()
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun postRestart() {
|
||||
view?.postDelayed(400) {
|
||||
activityRecreationHandle.recreateAll()
|
||||
|
||||
@@ -18,8 +18,10 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.firstNotNull
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.local.domain.DeleteReadChaptersUseCase
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import java.util.EnumMap
|
||||
@@ -33,6 +35,7 @@ class UserDataSettingsViewModel @Inject constructor(
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val settings: AppSettings,
|
||||
private val deleteReadChaptersUseCase: DeleteReadChaptersUseCase,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
@@ -44,6 +47,8 @@ class UserDataSettingsViewModel @Inject constructor(
|
||||
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
|
||||
val storageUsage = MutableStateFlow<StorageUsage?>(null)
|
||||
|
||||
val onChaptersCleanedUp = MutableEventFlow<Pair<Int, Long>>()
|
||||
|
||||
val periodicalBackupFrequency = settings.observeAsFlow(
|
||||
key = AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
|
||||
valueProducer = { isPeriodicalBackupEnabled },
|
||||
@@ -133,9 +138,24 @@ class UserDataSettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadStorageUsage() {
|
||||
fun cleanupChapters() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_CHAPTERS_CLEAR }
|
||||
val oldSize = storageUsage.firstNotNull().savedManga.bytes
|
||||
val chaptersCount = deleteReadChaptersUseCase.invoke()
|
||||
loadStorageUsage().join()
|
||||
val newSize = storageUsage.firstNotNull().savedManga.bytes
|
||||
onChaptersCleanedUp.call(chaptersCount to oldSize - newSize)
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_CHAPTERS_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadStorageUsage(): Job {
|
||||
val prevJob = storageUsageJob
|
||||
storageUsageJob = launchJob(Dispatchers.Default) {
|
||||
return launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
val pagesCacheSize = storageManager.computeCacheSize(CacheDir.PAGES)
|
||||
val otherCacheSize = storageManager.computeCacheSize() - pagesCacheSize
|
||||
@@ -160,6 +180,8 @@ class UserDataSettingsViewModel @Inject constructor(
|
||||
percent = (availableSpace.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
)
|
||||
}.also {
|
||||
storageUsageJob = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="?dialogPreferredPadding">
|
||||
android:paddingHorizontal="?dialogPreferredPadding"
|
||||
android:paddingTop="?dialogPreferredPadding">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_path_label"
|
||||
@@ -51,4 +52,13 @@
|
||||
app:drawableStartCompat="@drawable/bg_rounded_square"
|
||||
app:drawableTint="?colorSecondaryContainer" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_cleanup"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/delete_read_chapters"
|
||||
app:chipIcon="@drawable/ic_delete" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -623,4 +623,9 @@
|
||||
<string name="migrate_confirmation">Manga \"%1$s\" from \"%2$s\" will be replaced with \"%3$s\" from \"%4$s\" in your history and favorites (if present)</string>
|
||||
<string name="manga_migration">Manga migration</string>
|
||||
<string name="migration_completed">Migration completed</string>
|
||||
<string name="delete_read_chapters">Delete read chapters</string>
|
||||
<string name="no_chapters_deleted">No chapters have been deleted</string>
|
||||
<string name="chapters_deleted_pattern">Removed %1$s, cleared %2$s</string>
|
||||
<string name="delete_read_chapters_summary">Delete chapters you have already read from local storage to free up space</string>
|
||||
<string name="delete_read_chapters_prompt">This will permanently delete all chapters marked as read from your local storage. You can re-download it later, but the imported chapters may be lost forever</string>
|
||||
</resources>
|
||||
|
||||
@@ -83,6 +83,12 @@
|
||||
android:summary="@string/clear_cookies_summary"
|
||||
android:title="@string/clear_cookies" />
|
||||
|
||||
<Preference
|
||||
android:key="chapters_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/delete_read_chapters_summary"
|
||||
android:title="@string/delete_read_chapters" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user