Action to remove read local chapters

This commit is contained in:
Koitharu
2024-03-12 18:04:45 +02:00
parent 7e581a5ed7
commit 8313d6966f
10 changed files with 252 additions and 18 deletions

View File

@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.find 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.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter 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_PAGES_CACHE_CLEAR = "pages_cache_clear"
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear" const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
const val KEY_COOKIES_CLEAR = "cookies_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_THUMBS_CACHE_CLEAR = "thumbs_cache_clear"
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear" const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear" const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear"

View File

@@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.button.MaterialButton 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.progressindicator.BaseProgressIndicator
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
@@ -169,3 +170,11 @@ fun MaterialButton.setProgressIcon() {
icon = progressDrawable icon = progressDrawable
progressDrawable.start() progressDrawable.start()
} }
fun Chip.setProgressIcon() {
val progressDrawable = CircularProgressDrawable(context)
progressDrawable.strokeWidth = resources.resolveDp(2f)
progressDrawable.setColorSchemeColors(currentTextColor)
chipIcon = progressDrawable
progressDrawable.start()
}

View File

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

View File

@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.local.ui.info
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels 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.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView 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.FileSize
import org.koitharu.kotatsu.core.util.KotatsuColors
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.setProgressIcon
import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding 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 import com.google.android.material.R as materialR
@AndroidEntryPoint @AndroidEntryPoint
class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() { class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>(), View.OnClickListener {
private val viewModel: LocalInfoViewModel by viewModels() private val viewModel: LocalInfoViewModel by viewModels()
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
return super.onBuildDialog(builder) return super.onBuildDialog(builder).setTitle(R.string.saved_manga).setNegativeButton(R.string.close, null)
.setTitle(R.string.saved_manga)
.setNegativeButton(R.string.close, null)
} }
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogLocalInfoBinding { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogLocalInfoBinding {
@@ -43,13 +45,44 @@ class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() {
viewModel.path.observe(this) { viewModel.path.observe(this) {
binding.textViewPath.text = it 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) { if (it.first >= 0 && it.second >= 0) {
setSegments(it.first, it.second) setSegments(it.first, it.second)
} else { } else {
binding.barView.animateSegments(emptyList()) 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) { private fun setSegments(size: Long, available: Long) {

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.local.ui.info package org.koitharu.kotatsu.local.ui.info
import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -8,12 +7,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.BaseViewModel 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.computeSize
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.DeleteReadChaptersUseCase
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -21,21 +22,42 @@ class LocalInfoViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val storageManager: LocalStorageManager, private val storageManager: LocalStorageManager,
private val deleteReadChaptersUseCase: DeleteReadChaptersUseCase,
) : BaseViewModel() { ) : BaseViewModel() {
private val manga = savedStateHandle.require<ParcelableManga>(LocalInfoDialog.ARG_MANGA).manga 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 path = MutableStateFlow<String?>(null)
val size = MutableStateFlow(-1L) val size = MutableStateFlow(-1L)
val availableSize = MutableStateFlow(-1L) val availableSize = MutableStateFlow(-1L)
init { init {
launchLoadingJob(Dispatchers.Default) { computeSize()
val file = manga.url.toUri().toFileOrNull() ?: localMangaRepository.findSavedManga(manga)?.file }
requireNotNull(file)
path.value = file.path fun cleanup() {
size.value = file.computeSize() launchJob(Dispatchers.Default) {
availableSize.value = storageManager.computeAvailableSize() 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()
}
} }

View File

@@ -94,6 +94,7 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
} }
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this)) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView)) viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView))
viewModel.onChaptersCleanedUp.observeEvent(viewLifecycleOwner, ::onChaptersCleanedUp)
settings.subscribe(this) settings.subscribe(this)
} }
@@ -129,6 +130,11 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
true true
} }
AppSettings.KEY_CHAPTERS_CLEAR -> {
cleanupChapters()
true
}
AppSettings.KEY_UPDATES_FEED_CLEAR -> { AppSettings.KEY_UPDATES_FEED_CLEAR -> {
viewModel.clearUpdatesFeed() viewModel.clearUpdatesFeed()
true 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>) { private fun Preference.bindBytesSizeSummary(stateFlow: StateFlow<Long>) {
stateFlow.observe(viewLifecycleOwner) { size -> stateFlow.observe(viewLifecycleOwner) { size ->
summary = if (size < 0) { summary = if (size < 0) {
@@ -235,6 +256,16 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
}.show() }.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() { private fun postRestart() {
view?.postDelayed(400) { view?.postDelayed(400) {
activityRecreationHandle.recreateAll() activityRecreationHandle.recreateAll()

View File

@@ -18,8 +18,10 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call 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.CacheDir
import org.koitharu.kotatsu.local.data.LocalStorageManager 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.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import java.util.EnumMap import java.util.EnumMap
@@ -33,6 +35,7 @@ class UserDataSettingsViewModel @Inject constructor(
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val cookieJar: MutableCookieJar, private val cookieJar: MutableCookieJar,
private val settings: AppSettings, private val settings: AppSettings,
private val deleteReadChaptersUseCase: DeleteReadChaptersUseCase,
) : BaseViewModel() { ) : BaseViewModel() {
val onActionDone = MutableEventFlow<ReversibleAction>() val onActionDone = MutableEventFlow<ReversibleAction>()
@@ -44,6 +47,8 @@ class UserDataSettingsViewModel @Inject constructor(
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java) val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
val storageUsage = MutableStateFlow<StorageUsage?>(null) val storageUsage = MutableStateFlow<StorageUsage?>(null)
val onChaptersCleanedUp = MutableEventFlow<Pair<Int, Long>>()
val periodicalBackupFrequency = settings.observeAsFlow( val periodicalBackupFrequency = settings.observeAsFlow(
key = AppSettings.KEY_BACKUP_PERIODICAL_ENABLED, key = AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
valueProducer = { isPeriodicalBackupEnabled }, 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 val prevJob = storageUsageJob
storageUsageJob = launchJob(Dispatchers.Default) { return launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin() prevJob?.cancelAndJoin()
val pagesCacheSize = storageManager.computeCacheSize(CacheDir.PAGES) val pagesCacheSize = storageManager.computeCacheSize(CacheDir.PAGES)
val otherCacheSize = storageManager.computeCacheSize() - pagesCacheSize val otherCacheSize = storageManager.computeCacheSize() - pagesCacheSize
@@ -160,6 +180,8 @@ class UserDataSettingsViewModel @Inject constructor(
percent = (availableSpace.toDouble() / totalBytes).toFloat(), percent = (availableSpace.toDouble() / totalBytes).toFloat(),
), ),
) )
}.also {
storageUsageJob = it
} }
} }
} }

View File

@@ -6,7 +6,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="?dialogPreferredPadding"> android:paddingHorizontal="?dialogPreferredPadding"
android:paddingTop="?dialogPreferredPadding">
<TextView <TextView
android:id="@+id/textView_path_label" android:id="@+id/textView_path_label"
@@ -51,4 +52,13 @@
app:drawableStartCompat="@drawable/bg_rounded_square" app:drawableStartCompat="@drawable/bg_rounded_square"
app:drawableTint="?colorSecondaryContainer" /> 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> </LinearLayout>

View File

@@ -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="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="manga_migration">Manga migration</string>
<string name="migration_completed">Migration completed</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> </resources>

View File

@@ -83,6 +83,12 @@
android:summary="@string/clear_cookies_summary" android:summary="@string/clear_cookies_summary"
android:title="@string/clear_cookies" /> 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> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>