Delete local chapters in a service
This commit is contained in:
@@ -102,6 +102,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
|
|||||||
@@ -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?)
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import androidx.appcompat.widget.SearchView
|
|||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
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.adapter.ChaptersSelectionDecoration
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
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.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
@@ -160,8 +162,18 @@ class ChaptersFragment :
|
|||||||
}
|
}
|
||||||
R.id.action_delete -> {
|
R.id.action_delete -> {
|
||||||
val ids = selectionDecoration?.checkedItemsIds
|
val ids = selectionDecoration?.checkedItemsIds
|
||||||
if (!ids.isNullOrEmpty()) {
|
val manga = viewModel.manga.value
|
||||||
viewModel.deleteChapters(ids.toSet())
|
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()
|
mode.finish()
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ class DetailsActivity :
|
|||||||
viewModel.manga.observe(this, ::onMangaUpdated)
|
viewModel.manga.observe(this, ::onMangaUpdated)
|
||||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||||
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
|
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
|
||||||
viewModel.onChaptersRemoved.observe(this, ::onChaptersRemoved)
|
|
||||||
viewModel.onError.observe(this, ::onError)
|
viewModel.onError.observe(this, ::onError)
|
||||||
|
|
||||||
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
|
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
|
||||||
@@ -106,10 +105,6 @@ class DetailsActivity :
|
|||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onChaptersRemoved(count: Int) {
|
|
||||||
binding.snackbar.show(getString(R.string.removal_completed))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onError(e: Throwable) {
|
private fun onError(e: Throwable) {
|
||||||
when {
|
when {
|
||||||
ExceptionResolver.canResolve(e) -> {
|
ExceptionResolver.canResolve(e) -> {
|
||||||
@@ -179,16 +174,15 @@ class DetailsActivity :
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_delete -> {
|
R.id.action_delete -> {
|
||||||
viewModel.manga.value?.let { m ->
|
val title = viewModel.manga.value?.title.orEmpty()
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.delete_manga)
|
.setTitle(R.string.delete_manga)
|
||||||
.setMessage(getString(R.string.text_delete_local_manga, m.title))
|
.setMessage(getString(R.string.text_delete_local_manga, title))
|
||||||
.setPositiveButton(R.string.delete) { _, _ ->
|
.setPositiveButton(R.string.delete) { _, _ ->
|
||||||
viewModel.deleteLocal(m)
|
viewModel.deleteLocal()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_save -> {
|
R.id.action_save -> {
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ class DetailsViewModel(
|
|||||||
.asLiveData(viewModelScope.coroutineContext)
|
.asLiveData(viewModelScope.coroutineContext)
|
||||||
|
|
||||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
val onMangaRemoved = SingleLiveEvent<Manga>()
|
||||||
val onChaptersRemoved = SingleLiveEvent<Int>()
|
|
||||||
|
|
||||||
val branches = mangaData.map {
|
val branches = mangaData.map {
|
||||||
it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
|
it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
|
||||||
@@ -136,8 +135,11 @@ class DetailsViewModel(
|
|||||||
loadingJob = doLoad()
|
loadingJob = doLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteLocal(manga: Manga) {
|
fun deleteLocal() {
|
||||||
|
val m = mangaData.value ?: return
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
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)
|
val original = localMangaRepository.getRemoteManga(manga)
|
||||||
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -185,19 +187,6 @@ class DetailsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteChapters(ids: Set<Long>) {
|
|
||||||
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) {
|
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||||
var manga = mangaDataRepository.resolveIntent(intent)
|
var manga = mangaDataRepository.resolveIntent(intent)
|
||||||
?: throw MangaNotFoundException("Cannot find manga")
|
?: throw MangaNotFoundException("Cannot find manga")
|
||||||
|
|||||||
@@ -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<LocalMangaRepository>()
|
||||||
|
|
||||||
|
override suspend fun processIntent(intent: Intent?) {
|
||||||
|
val manga = intent?.getParcelableExtra<ParcelableManga>(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<Long>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,4 +274,6 @@
|
|||||||
<string name="parallel_downloads">Загружать параллельно</string>
|
<string name="parallel_downloads">Загружать параллельно</string>
|
||||||
<string name="download_slowdown">Замедление загрузки</string>
|
<string name="download_slowdown">Замедление загрузки</string>
|
||||||
<string name="download_slowdown_summary">Помогает избежать блокировки IP-адреса</string>
|
<string name="download_slowdown_summary">Помогает избежать блокировки IP-адреса</string>
|
||||||
|
<string name="local_manga_processing">Обработка сохранённой манги</string>
|
||||||
|
<string name="chapters_will_removed_background">Главы будут удалены в фоновом режиме. Это может занять какое-то время</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -277,4 +277,6 @@
|
|||||||
<string name="parallel_downloads">Parallel downloads</string>
|
<string name="parallel_downloads">Parallel downloads</string>
|
||||||
<string name="download_slowdown">Download slowdown</string>
|
<string name="download_slowdown">Download slowdown</string>
|
||||||
<string name="download_slowdown_summary">Helps avoid blocking your IP address</string>
|
<string name="download_slowdown_summary">Helps avoid blocking your IP address</string>
|
||||||
|
<string name="local_manga_processing">Saved manga processing</string>
|
||||||
|
<string name="chapters_will_removed_background">Chapters will be removed in the background. It can take some time</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user