diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index 63518ad17..f6bab4ce2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -10,7 +10,6 @@ import android.provider.OpenableColumns import androidx.core.database.getStringOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible -import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.fs.FileSequence import java.io.File @@ -53,7 +52,7 @@ fun File.getStorageName(context: Context): String = runCatching { fun Uri.toFileOrNull() = if (scheme == URI_SCHEME_FILE) path?.let(::File) else null -suspend fun File.deleteAwait() = withContext(Dispatchers.IO) { +suspend fun File.deleteAwait() = runInterruptible(Dispatchers.IO) { delete() || deleteRecursively() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 679652100..cd0b4c8eb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.plus @@ -144,6 +145,7 @@ class DetailsViewModel @Inject constructor( val localSize = details .map { it?.local } .distinctUntilChanged() + .combine(localStorageChanges.onStart { emit(null) }) { x, _ -> x } .map { local -> if (local != null) { runCatchingCancellable { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt index 712e9ff37..c859ffe66 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt @@ -106,7 +106,7 @@ class MangaIndex(source: String?) { } fun removeChapter(id: Long): Boolean { - return json.getJSONObject("chapters").remove(id.toString()) != null + return json.has("chapters") && json.getJSONObject("chapters").remove(id.toString()) != null } fun getChapterFileName(chapterId: Long): String? { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt index 4310ba7ed..dc9904cfc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt @@ -1,5 +1,7 @@ package org.koitharu.kotatsu.local.data.output +import androidx.core.net.toFile +import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex @@ -9,6 +11,7 @@ import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.zip.ZipOutput import org.koitharu.kotatsu.local.data.MangaIndex +import org.koitharu.kotatsu.local.data.input.LocalMangaDirInput import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.toFileNameSafe @@ -46,22 +49,23 @@ class LocalMangaDirOutput( flushIndex() } - override suspend fun addPage(chapter: IndexedValue, file: File, pageNumber: Int, ext: String) = mutex.withLock { - val output = chaptersOutput.getOrPut(chapter.value) { - ZipOutput(File(rootFile, chapterFileName(chapter) + SUFFIX_TMP)) - } - val name = buildString { - append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber)) - if (ext.isNotEmpty() && ext.length <= 4) { - append('.') - append(ext) + override suspend fun addPage(chapter: IndexedValue, file: File, pageNumber: Int, ext: String) = + mutex.withLock { + val output = chaptersOutput.getOrPut(chapter.value) { + ZipOutput(File(rootFile, chapterFileName(chapter) + SUFFIX_TMP)) } + val name = buildString { + append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber)) + if (ext.isNotEmpty() && ext.length <= 4) { + append('.') + append(ext) + } + } + runInterruptible(Dispatchers.IO) { + output.put(name, file) + } + index.addChapter(chapter, chapterFileName(chapter)) } - runInterruptible(Dispatchers.IO) { - output.put(name, file) - } - index.addChapter(chapter, chapterFileName(chapter)) - } override suspend fun flushChapter(chapter: MangaChapter): Boolean = mutex.withLock { val output = chaptersOutput.remove(chapter) ?: return@withLock false @@ -90,13 +94,24 @@ class LocalMangaDirOutput( } } - suspend fun deleteChapter(chapterId: Long) = mutex.withLock { - val chapter = checkNotNull(index.getMangaInfo()?.chapters?.withIndex()) { + suspend fun deleteChapters(ids: Set) = mutex.withLock { + val chapters = checkNotNull((index.getMangaInfo() ?: LocalMangaDirInput(rootFile).getManga().manga).chapters) { "No chapters found" - }.find { x -> x.value.id == chapterId } ?: error("Chapter not found") - val chapterDir = File(rootFile, chapterFileName(chapter)) - chapterDir.deleteAwait() - index.removeChapter(chapterId) + }.withIndex() + val victimsIds = ids.toMutableSet() + for (chapter in chapters) { + if (!victimsIds.remove(chapter.value.id)) { + continue + } + val chapterFile = index.getChapterFileName(chapter.value.id)?.let { + File(rootFile, it) + } ?: chapter.value.url.toUri().toFile() + chapterFile.deleteAwait() + index.removeChapter(chapter.value.id) + } + check(victimsIds.isEmpty()) { + "${victimsIds.size} of ${ids.size} chapters was not removed: not found" + } } fun setIndex(newIndex: MangaIndex) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt index baa354cfa..d2fb72122 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt @@ -25,9 +25,7 @@ class LocalMangaUtil( } is LocalMangaDirOutput -> { - for (id in ids) { - output.deleteChapter(id) - } + output.deleteChapters(ids) output.finish() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt index b19b955da..6078bac27 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt @@ -11,6 +11,7 @@ import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.MutableSharedFlow import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ErrorReporterReceiver import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.ui.CoroutineIntentService import org.koitharu.kotatsu.core.util.ext.getDisplayMessage @@ -45,10 +46,13 @@ class LocalChaptersRemoveService : CoroutineIntentService() { val manga = intent.getParcelableExtraCompat(EXTRA_MANGA)?.manga ?: return val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return startForeground() - val mangaWithChapters = localMangaRepository.getDetails(manga) - localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds) - localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga))) - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + try { + val mangaWithChapters = localMangaRepository.getDetails(manga) + localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds) + localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga))) + } finally { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + } } override fun onError(startId: Int, error: Throwable) { @@ -60,6 +64,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() { .setContentText(error.getDisplayMessage(resources)) .setSmallIcon(android.R.drawable.stat_notify_error) .setAutoCancel(true) + .setContentIntent(ErrorReporterReceiver.getPendingIntent(this, error)) .build() val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager nm.notify(NOTIFICATION_ID + startId, notification)