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.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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user