diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6c6c112b8..bba1d89b0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -102,6 +102,7 @@
+
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt
new file mode 100644
index 000000000..241d13f94
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt
@@ -0,0 +1,37 @@
+package org.koitharu.kotatsu.base.ui
+
+import android.app.Service
+import android.content.Intent
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+
+abstract class CoroutineIntentService : BaseService() {
+
+ private val mutex = Mutex()
+ protected open val dispatcher: CoroutineDispatcher = Dispatchers.Default
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+ launchCoroutine(intent, startId)
+ return Service.START_REDELIVER_INTENT
+ }
+
+ private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch {
+ mutex.withLock {
+ try {
+ withContext(dispatcher) {
+ processIntent(intent)
+ }
+ } finally {
+ stopSelf(startId)
+ }
+ }
+ }
+
+ protected abstract suspend fun processIntent(intent: Intent?)
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
index 143a87713..91698a76b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
@@ -11,6 +11,7 @@ import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
+import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -21,6 +22,7 @@ import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.download.ui.service.DownloadService
+import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -160,8 +162,18 @@ class ChaptersFragment :
}
R.id.action_delete -> {
val ids = selectionDecoration?.checkedItemsIds
- if (!ids.isNullOrEmpty()) {
- viewModel.deleteChapters(ids.toSet())
+ val manga = viewModel.manga.value
+ when {
+ ids.isNullOrEmpty() || manga == null -> Unit
+ ids.size == manga.chapters?.size -> viewModel.deleteLocal()
+ else -> {
+ LocalChaptersRemoveService.start(requireContext(), manga, ids)
+ Snackbar.make(
+ binding.recyclerViewChapters,
+ R.string.chapters_will_removed_background,
+ Snackbar.LENGTH_LONG
+ ).show()
+ }
}
mode.finish()
true
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
index b29596141..a1920bf80 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
@@ -82,7 +82,6 @@ class DetailsActivity :
viewModel.manga.observe(this, ::onMangaUpdated)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
- viewModel.onChaptersRemoved.observe(this, ::onChaptersRemoved)
viewModel.onError.observe(this, ::onError)
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
@@ -106,10 +105,6 @@ class DetailsActivity :
finishAfterTransition()
}
- private fun onChaptersRemoved(count: Int) {
- binding.snackbar.show(getString(R.string.removal_completed))
- }
-
private fun onError(e: Throwable) {
when {
ExceptionResolver.canResolve(e) -> {
@@ -179,16 +174,15 @@ class DetailsActivity :
true
}
R.id.action_delete -> {
- viewModel.manga.value?.let { m ->
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.delete_manga)
- .setMessage(getString(R.string.text_delete_local_manga, m.title))
- .setPositiveButton(R.string.delete) { _, _ ->
- viewModel.deleteLocal(m)
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- }
+ val title = viewModel.manga.value?.title.orEmpty()
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.delete_manga)
+ .setMessage(getString(R.string.text_delete_local_manga, title))
+ .setPositiveButton(R.string.delete) { _, _ ->
+ viewModel.deleteLocal()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
true
}
R.id.action_save -> {
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index cba0e11b6..0ee363efd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -86,7 +86,6 @@ class DetailsViewModel(
.asLiveData(viewModelScope.coroutineContext)
val onMangaRemoved = SingleLiveEvent()
- val onChaptersRemoved = SingleLiveEvent()
val branches = mangaData.map {
it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
@@ -136,8 +135,11 @@ class DetailsViewModel(
loadingJob = doLoad()
}
- fun deleteLocal(manga: Manga) {
+ fun deleteLocal() {
+ val m = mangaData.value ?: return
launchLoadingJob(Dispatchers.Default) {
+ val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)
+ checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
val original = localMangaRepository.getRemoteManga(manga)
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
runCatching {
@@ -185,19 +187,6 @@ class DetailsViewModel(
}
}
- fun deleteChapters(ids: Set) {
- val m = mangaData.value ?: return
- if (m.chapters?.size == ids.size) {
- deleteLocal(m)
- return
- }
- launchLoadingJob {
- localMangaRepository.deleteChapters(m, ids)
- reload()
- onChaptersRemoved.call(ids.size)
- }
- }
-
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
var manga = mangaDataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga")
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt
new file mode 100644
index 000000000..3bc53726c
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt
@@ -0,0 +1,80 @@
+package org.koitharu.kotatsu.local.ui
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import androidx.core.content.ContextCompat
+import org.koin.android.ext.android.inject
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.CoroutineIntentService
+import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
+import org.koitharu.kotatsu.download.ui.service.DownloadService
+import org.koitharu.kotatsu.local.domain.LocalMangaRepository
+import org.koitharu.kotatsu.parsers.model.Manga
+
+class LocalChaptersRemoveService : CoroutineIntentService() {
+
+ private val localMangaRepository by inject()
+
+ override suspend fun processIntent(intent: Intent?) {
+ val manga = intent?.getParcelableExtra(EXTRA_MANGA)?.manga ?: return
+ val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return
+ startForeground()
+ val mangaWithChapters = localMangaRepository.getDetails(manga)
+ localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
+ sendBroadcast(
+ Intent(DownloadService.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
+ )
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
+ }
+
+ private fun startForeground() {
+ val title = getString(R.string.local_manga_processing)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val channel = NotificationChannel(CHANNEL_ID, title, NotificationManager.IMPORTANCE_LOW)
+ channel.setShowBadge(false)
+ channel.enableVibration(false)
+ channel.setSound(null, null)
+ channel.enableLights(false)
+ manager.createNotificationChannel(channel)
+ }
+
+ val notification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(title)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setDefaults(0)
+ .setColor(ContextCompat.getColor(this, R.color.blue_primary_dark))
+ .setSilent(true)
+ .setProgress(0, 0, true)
+ .setSmallIcon(android.R.drawable.stat_notify_sync)
+ .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
+ .setOngoing(true)
+ .build()
+ startForeground(NOTIFICATION_ID, notification)
+ }
+
+ companion object {
+
+ private const val CHANNEL_ID = "local_processing"
+ private const val NOTIFICATION_ID = 21
+
+ private const val EXTRA_MANGA = "manga"
+ private const val EXTRA_CHAPTERS_IDS = "chapters_ids"
+
+ fun start(context: Context, manga: Manga, chaptersIds: Collection) {
+ if (chaptersIds.isEmpty()) {
+ return
+ }
+ val intent = Intent(context, LocalChaptersRemoveService::class.java)
+ intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
+ intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
+ ContextCompat.startForegroundService(context, intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index a4c27b6a0..3b28783b5 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -274,4 +274,6 @@
Загружать параллельно
Замедление загрузки
Помогает избежать блокировки IP-адреса
+ Обработка сохранённой манги
+ Главы будут удалены в фоновом режиме. Это может занять какое-то время
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e249d13f7..828ad9fe6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -277,4 +277,6 @@
Parallel downloads
Download slowdown
Helps avoid blocking your IP address
+ Saved manga processing
+ Chapters will be removed in the background. It can take some time
\ No newline at end of file