From e5c765dd2fab743026ce4b1836b7d7b7358ee326 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 11 Oct 2024 09:57:58 +0300 Subject: [PATCH 1/8] Update parsers --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 02ee0fe0e..a88b37f0f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 676 - versionName = '7.6.3' + versionCode = 677 + versionName = '7.6.4' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -82,7 +82,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:1ebb298cd7') { + implementation('com.github.KotatsuApp:kotatsu-parsers:197b148fca') { exclude group: 'org.json', module: 'json' } From 05d22167c4495aa39f4b29fe38cbdf0a3beb5b21 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 11 Oct 2024 10:15:29 +0300 Subject: [PATCH 2/8] Fix skipping download errors --- .../koitharu/kotatsu/download/ui/worker/DownloadWorker.kt | 5 ++++- .../org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index 9fe15de56..e29f6de14 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -311,6 +311,10 @@ class DownloadWorker @AssistedInject constructor( DOWNLOAD_ERROR_DELAY } if (countDown <= 0 || retryDelay < 0 || retryDelay > MAX_RETRY_DELAY) { + val pausingHandle = PausingHandle.current() + if (pausingHandle.skipAllErrors()) { + return null + } publishState( currentState.copy( isPaused = true, @@ -321,7 +325,6 @@ class DownloadWorker @AssistedInject constructor( ), ) countDown = MAX_FAILSAFE_ATTEMPTS - val pausingHandle = PausingHandle.current() pausingHandle.pause() try { pausingHandle.awaitResumed() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt index e02205230..3eb184121 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt @@ -53,7 +53,9 @@ class PausingHandle : AbstractCoroutineContextElement(PausingHandle) { } } - fun skipCurrentError(): Boolean = skipError.compareAndSet(expect = true, update = skipAllErrors) + fun skipAllErrors(): Boolean = skipAllErrors + + fun skipCurrentError(): Boolean = skipError.compareAndSet(expect = true, update = false) companion object : CoroutineContext.Key { From e515069b5361e4b6a7e689cc5e7cce9df388ed3e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 11 Oct 2024 10:55:47 +0300 Subject: [PATCH 3/8] Fix zip closing (cherry picked from commit 144e66bedb5618c0041acd955d11f7ffba05c436) --- .../kotatsu/core/backup/BackupZipInput.kt | 15 +++------ .../koitharu/kotatsu/core/util/ext/File.kt | 8 ----- .../org/koitharu/kotatsu/core/util/ext/Uri.kt | 10 ++++-- .../ui/pager/pages/MangaPageFetcher.kt | 29 ++++++++++------- .../koitharu/kotatsu/local/data/CbzFetcher.kt | 31 +++++++++++-------- .../settings/backup/RestoreViewModel.kt | 7 ++++- 6 files changed, 53 insertions(+), 47 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipInput.kt index 2fb7cd110..a1499afc7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipInput.kt @@ -1,14 +1,11 @@ package org.koitharu.kotatsu.core.backup -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible import okhttp3.internal.closeQuietly import okio.Closeable import org.json.JSONArray import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException -import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import java.io.File import java.util.EnumSet import java.util.zip.ZipException @@ -36,13 +33,9 @@ class BackupZipInput private constructor(val file: File) : Closeable { zipFile.close() } - fun cleanupAsync() { - processLifecycleScope.launch(Dispatchers.IO, CoroutineStart.ATOMIC) { - runCatching { - closeQuietly() - file.delete() - } - } + fun closeAndDelete() { + closeQuietly() + file.delete() } companion object { @@ -55,7 +48,7 @@ class BackupZipInput private constructor(val file: File) : Closeable { throw BadBackupFormatException(null) } res - } catch (exception: Exception) { + } catch (exception: Throwable) { res?.closeQuietly() throw if (exception is ZipException) { BadBackupFormatException(exception) 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 2343f8ec7..174b0715c 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 @@ -39,14 +39,6 @@ fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().u it.readText() } -@Blocking -fun ZipFile.getInputStreamOrClose(entry: ZipEntry): InputStream = try { - getInputStream(entry) -} catch (e: Throwable) { - closeQuietly() - throw e -} - fun File.getStorageName(context: Context): String = runCatching { val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt index 152aebd14..5664a180d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.util.ext import android.net.Uri import androidx.core.net.toFile +import okhttp3.internal.closeQuietly import okio.Source import okio.source import okio.use @@ -40,8 +41,13 @@ fun Uri.source(): Source = when (scheme) { URI_SCHEME_FILE -> toFile().source() URI_SCHEME_ZIP -> { val zip = ZipFile(schemeSpecificPart) - val entry = zip.getEntry(fragment) - zip.getInputStreamOrClose(entry).source().withExtraCloseable(zip) + try { + val entry = zip.getEntry(fragment) + zip.getInputStream(entry).source().withExtraCloseable(zip) + } catch (e: Throwable) { + zip.closeQuietly() + throw e + } } else -> unsupportedUri(this) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt index e2d541e64..a5d8b7082 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt @@ -16,13 +16,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okhttp3.OkHttpClient +import okhttp3.internal.closeQuietly import okio.Path.Companion.toOkioPath import okio.buffer import okio.source import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.util.ext.getInputStreamOrClose import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.isFileUri import org.koitharu.kotatsu.local.data.isZipUri @@ -67,17 +67,22 @@ class MangaPageFetcher( return when { uri.isZipUri() -> runInterruptible(Dispatchers.IO) { val zip = ZipFile(uri.schemeSpecificPart) - val entry = zip.getEntry(uri.fragment) - SourceResult( - source = ImageSource( - source = zip.getInputStreamOrClose(entry).source().withExtraCloseable(zip).buffer(), - context = context, - metadata = MangaPageMetadata(page), - ), - mimeType = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension(entry.name.substringAfterLast('.', "")), - dataSource = DataSource.DISK, - ) + try { + val entry = zip.getEntry(uri.fragment) + SourceResult( + source = ImageSource( + source = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer(), + context = context, + metadata = MangaPageMetadata(page), + ), + mimeType = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(entry.name.substringAfterLast('.', "")), + dataSource = DataSource.DISK, + ) + } catch (e: Throwable) { + zip.closeQuietly() + throw e + } } uri.isFileUri() -> runInterruptible(Dispatchers.IO) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFetcher.kt index 0ce579bd3..c80fefa1e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFetcher.kt @@ -10,9 +10,9 @@ import coil.fetch.SourceResult import coil.request.Options import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import okhttp3.internal.closeQuietly import okio.buffer import okio.source -import org.koitharu.kotatsu.core.util.ext.getInputStreamOrClose import org.koitharu.kotatsu.local.data.util.withExtraCloseable import java.util.zip.ZipFile @@ -23,18 +23,23 @@ class CbzFetcher( override suspend fun fetch() = runInterruptible(Dispatchers.IO) { val zip = ZipFile(uri.schemeSpecificPart) - val entry = zip.getEntry(uri.fragment) - val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name) - val bufferedSource = zip.getInputStreamOrClose(entry).source().withExtraCloseable(zip).buffer() - SourceResult( - source = ImageSource( - source = bufferedSource, - context = options.context, - metadata = CbzMetadata(uri), - ), - mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext), - dataSource = DataSource.DISK, - ) + try { + val entry = zip.getEntry(uri.fragment) + val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name) + val bufferedSource = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer() + SourceResult( + source = ImageSource( + source = bufferedSource, + context = options.context, + metadata = CbzMetadata(uri), + ), + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext), + dataSource = DataSource.DISK, + ) + } catch (e: Throwable) { + zip.closeQuietly() + throw e + } } class Factory : Fetcher.Factory { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt index bef9212ad..3af75946f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt @@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.backup.CompositeResult import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.parsers.util.SuspendLazy import java.io.File @@ -71,7 +72,11 @@ class RestoreViewModel @Inject constructor( override fun onCleared() { super.onCleared() - backupInput.peek()?.cleanupAsync() + runCatching { + backupInput.peek()?.closeAndDelete() + }.onFailure { + it.printStackTraceDebug() + } } fun onItemClick(item: BackupEntryModel) { From d28eff7a7506e3b4348124a207efb6d4e27d6102 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 13 Oct 2024 09:39:28 +0300 Subject: [PATCH 4/8] Fix zip closing --- .../koitharu/kotatsu/core/zip/ZipOutput.kt | 14 ++- .../local/data/output/LocalMangaDirOutput.kt | 22 +++- .../local/data/output/LocalMangaUtil.kt | 24 +--- .../local/data/output/LocalMangaZipOutput.kt | 104 ++++++++++-------- 4 files changed, 91 insertions(+), 73 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt index 82378614a..e1053bfc5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt @@ -103,8 +103,11 @@ class ZipOutput( } val zipEntry = ZipEntry(name) putNextEntry(zipEntry) - fis.copyTo(this) - closeEntry() + try { + fis.copyTo(this) + } finally { + closeEntry() + } } } return true @@ -117,8 +120,11 @@ class ZipOutput( } val zipEntry = ZipEntry(name) putNextEntry(zipEntry) - content.byteInputStream().copyTo(this) - closeEntry() + try { + content.byteInputStream().copyTo(this) + } finally { + closeEntry() + } return true } } 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 32501d50d..680df9432 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 @@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import okhttp3.internal.closeQuietly import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.takeIfReadable @@ -90,7 +91,7 @@ class LocalMangaDirOutput( override fun close() { for (output in chaptersOutput.values) { - output.close() + output.closeQuietly() } } @@ -119,10 +120,21 @@ class LocalMangaDirOutput( } private suspend fun ZipOutput.flushAndFinish() = runInterruptible(Dispatchers.IO) { - finish() - close() - val resFile = File(file.absolutePath.removeSuffix(SUFFIX_TMP)) - file.renameTo(resFile) + val e: Throwable? = try { + finish() + null + } catch (e: Throwable) { + e + } finally { + close() + } + if (e == null) { + val resFile = File(file.absolutePath.removeSuffix(SUFFIX_TMP)) + file.renameTo(resFile) + } else { + file.delete() + throw e + } } private fun chapterFileName(chapter: IndexedValue): String { 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 08df5832a..011f4ff6f 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 @@ -2,8 +2,6 @@ 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 org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.parsers.model.Manga @@ -16,26 +14,14 @@ class LocalMangaUtil( } suspend fun deleteChapters(ids: Set) { - newOutput().use { output -> - when (output) { - is LocalMangaZipOutput -> runInterruptible(Dispatchers.IO) { - LocalMangaZipOutput.filterChapters(output, ids) - } - - is LocalMangaDirOutput -> { - output.deleteChapters(ids) - output.finish() - } - } - } - } - - private suspend fun newOutput(): LocalMangaOutput = runInterruptible(Dispatchers.IO) { val file = manga.url.toUri().toFile() if (file.isDirectory) { - LocalMangaDirOutput(file, manga) + LocalMangaDirOutput(file, manga).use { output -> + output.deleteChapters(ids) + output.finish() + } } else { - LocalMangaZipOutput(file, manga) + LocalMangaZipOutput.filterChapters(file, manga, ids) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt index d4370c3f6..eded64595 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import okhttp3.internal.closeQuietly import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.readText @@ -52,27 +53,29 @@ class LocalMangaZipOutput( index.setCoverEntry(name) } - override suspend fun addPage(chapter: IndexedValue, file: File, pageNumber: Int, ext: String) = mutex.withLock { - 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 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, null) } - runInterruptible(Dispatchers.IO) { - output.put(name, file) - } - index.addChapter(chapter, null) - } override suspend fun flushChapter(chapter: MangaChapter): Boolean = false override suspend fun finish() = mutex.withLock { runInterruptible(Dispatchers.IO) { - output.put(ENTRY_NAME_INDEX, index.toString()) - output.finish() - output.close() + output.use { output -> + output.put(ENTRY_NAME_INDEX, index.toString()) + output.finish() + } } rootFile.deleteAwait() output.file.renameTo(rootFile) @@ -115,42 +118,53 @@ class LocalMangaZipOutput( private const val FILENAME_PATTERN = "%08d_%03d%03d" - @WorkerThread - fun filterChapters(subject: LocalMangaZipOutput, idsToRemove: Set) { - ZipFile(subject.rootFile).use { zip -> - val index = MangaIndex(zip.readText(zip.getEntry(ENTRY_NAME_INDEX))) - idsToRemove.forEach { id -> index.removeChapter(id) } - val patterns = requireNotNull(index.getMangaInfo()?.chapters).map { - index.getChapterNamesPattern(it) - } - val coverEntryName = index.getCoverEntry() - for (entry in zip.entries()) { - when { - entry.name == ENTRY_NAME_INDEX -> { - subject.output.put(ENTRY_NAME_INDEX, index.toString()) - } - - entry.isDirectory -> { - subject.output.addDirectory(entry.name) - } - - entry.name == coverEntryName -> { - subject.output.copyEntryFrom(zip, entry) + suspend fun filterChapters(file: File, manga: Manga, idsToRemove: Set) = + runInterruptible(Dispatchers.IO) { + val subject = LocalMangaZipOutput(file, manga) + try { + ZipFile(subject.rootFile).use { zip -> + val index = MangaIndex(zip.readText(zip.getEntry(ENTRY_NAME_INDEX))) + idsToRemove.forEach { id -> index.removeChapter(id) } + val patterns = requireNotNull(index.getMangaInfo()?.chapters).map { + index.getChapterNamesPattern(it) } - - else -> { - val name = entry.name.substringBefore('.') - if (patterns.any { it.matches(name) }) { - subject.output.copyEntryFrom(zip, entry) + val coverEntryName = index.getCoverEntry() + for (entry in zip.entries()) { + when { + entry.name == ENTRY_NAME_INDEX -> { + subject.output.put(ENTRY_NAME_INDEX, index.toString()) + } + + entry.isDirectory -> { + subject.output.addDirectory(entry.name) + } + + entry.name == coverEntryName -> { + subject.output.copyEntryFrom(zip, entry) + } + + else -> { + val name = entry.name.substringBefore('.') + if (patterns.any { it.matches(name) }) { + subject.output.copyEntryFrom(zip, entry) + } + } } } + subject.output.finish() + subject.output.close() + subject.rootFile.delete() + subject.output.file.renameTo(subject.rootFile) } + } catch (e: Throwable) { + subject.closeQuietly() + try { + subject.output.file.delete() + } catch (e2: Throwable) { + e.addSuppressed(e2) + } + throw e } - subject.output.finish() - subject.output.close() - subject.rootFile.delete() - subject.output.file.renameTo(subject.rootFile) } - } } } From be4fb3e87313fac9ad49a4fd9a775bbcd01f4965 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 13 Oct 2024 09:43:01 +0300 Subject: [PATCH 5/8] Fix saving cover --- .../kotatsu/core/network/CommonHeadersInterceptor.kt | 9 +++++---- .../org/koitharu/kotatsu/image/ui/ImageViewModel.kt | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt index 8a8094a14..30ca3e95a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt @@ -9,6 +9,7 @@ import okhttp3.Request import okhttp3.Response import okio.IOException import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.model.UnknownMangaSource import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository @@ -29,13 +30,13 @@ class CommonHeadersInterceptor @Inject constructor( override fun intercept(chain: Chain): Response { val request = chain.request() val source = request.tag(MangaSource::class.java) - val repository = if (source != null) { - mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository - } else { - if (BuildConfig.DEBUG) { + val repository = if (source == null || source == UnknownMangaSource) { + if (BuildConfig.DEBUG && source == null) { Log.w("Http", "Request without source tag: ${request.url}") } null + } else { + mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository } val headersBuilder = request.headers.newBuilder() repository?.getRequestHeaders()?.let { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageViewModel.kt index 167f635aa..cde609d67 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageViewModel.kt @@ -12,6 +12,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.MutableEventFlow @@ -36,7 +37,7 @@ class ImageViewModel @Inject constructor( .memoryCachePolicy(CachePolicy.READ_ONLY) .data(savedStateHandle.require(BaseActivity.EXTRA_DATA)) .memoryCachePolicy(CachePolicy.DISABLED) - .source(savedStateHandle[ImageActivity.EXTRA_SOURCE]) + .source(MangaSource(savedStateHandle[ImageActivity.EXTRA_SOURCE])) .build() val bitmap = coil.execute(request).getDrawableOrThrow().toBitmap() runInterruptible(Dispatchers.IO) { From 8c5231bb3d26a6a99465551a4b91d8d8e869afdf Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 13 Oct 2024 14:08:25 +0300 Subject: [PATCH 6/8] Fix read chapters deletion --- .../local/domain/DeleteReadChaptersUseCase.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteReadChaptersUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteReadChaptersUseCase.kt index 877395a40..d19b724ba 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteReadChaptersUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteReadChaptersUseCase.kt @@ -10,18 +10,22 @@ 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.parser.MangaRepository 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.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.recoverCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import javax.inject.Inject class DeleteReadChaptersUseCase @Inject constructor( private val localMangaRepository: LocalMangaRepository, private val historyRepository: HistoryRepository, + private val mangaRepositoryFactory: MangaRepository.Factory, @LocalStorageChanges private val localStorageChanges: MutableSharedFlow, ) { @@ -68,8 +72,8 @@ class DeleteReadChaptersUseCase @Inject constructor( 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()) { + val chapters = getAllChapters(manga) + if (chapters.isEmpty()) { return null } val branch = (chapters.findById(history.chapterId) ?: return null).branch @@ -89,6 +93,21 @@ class DeleteReadChaptersUseCase @Inject constructor( localStorageChanges.emit(subject.copy(manga = updated)) } + private suspend fun getAllChapters(manga: LocalManga): List = runCatchingCancellable { + val remoteManga = checkNotNull(localMangaRepository.getRemoteManga(manga.manga)) + checkNotNull(mangaRepositoryFactory.create(remoteManga.source).getDetails(remoteManga).chapters) + }.recoverCatchingCancellable { + checkNotNull( + manga.manga.chapters.let { + if (it.isNullOrEmpty()) { + localMangaRepository.getDetails(manga.manga).chapters + } else { + it + } + }, + ) + }.getOrDefault(manga.manga.chapters.orEmpty()) + private class DeletionTask( val manga: LocalManga, val chaptersIds: Set, From 32eb273fa93cdce91cd9ecaf82e4101ac05999f0 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 13 Oct 2024 15:47:43 +0300 Subject: [PATCH 7/8] Update parsers --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a88b37f0f..105aa4ac9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:197b148fca') { + implementation('com.github.KotatsuApp:kotatsu-parsers:1.2.2') { exclude group: 'org.json', module: 'json' } From d739e30c84f1661472f270f8df910b4098de80c1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 7 Oct 2024 20:02:34 +0300 Subject: [PATCH 8/8] Improve filter --- .../kotatsu/filter/ui/FilterCoordinator.kt | 9 +++ .../kotatsu/filter/ui/FilterFieldLayout.kt | 5 ++ .../kotatsu/filter/ui/FilterHeaderFragment.kt | 13 ++++ .../kotatsu/filter/ui/FilterHeaderProducer.kt | 75 +++++++++++++++---- .../filter/ui/sheet/FilterSheetFragment.kt | 9 +++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 98 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 110bf6373..ec95fea68 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -332,6 +332,15 @@ class FilterCoordinator @Inject constructor( } } + fun toggleDemographic(value: Demographic, isSelected: Boolean) { + currentListFilter.update { oldValue -> + oldValue.copy( + demographics = if (isSelected) oldValue.demographics + value else oldValue.demographics - value, + query = oldValue.takeQueryIfSupported(), + ) + } + } + fun toggleContentType(value: ContentType, isSelected: Boolean) { currentListFilter.update { oldValue -> oldValue.copy( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterFieldLayout.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterFieldLayout.kt index 90ca05cd8..b5b1f83c0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterFieldLayout.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterFieldLayout.kt @@ -8,6 +8,7 @@ import android.view.View import android.widget.RelativeLayout import android.widget.TextView import androidx.annotation.AttrRes +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes import androidx.core.view.isInvisible @@ -68,6 +69,10 @@ class FilterFieldLayout @JvmOverloads constructor( } } + fun setTitle(@StringRes titleResId: Int) { + binding.textViewTitle.setText(titleResId) + } + fun setError(errorMessage: String?) { if (errorMessage == null && errorView == null) { return diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt index 0dc0271e7..ff7d30917 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -16,7 +16,13 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet +import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Demographic +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN +import java.util.Locale import javax.inject.Inject @AndroidEntryPoint @@ -55,6 +61,13 @@ class FilterHeaderFragment : BaseFragment(), ChipsV override fun onChipCloseClick(chip: Chip, data: Any?) { when (data) { is String -> filter.setQuery(null) + is ContentRating -> filter.toggleContentRating(data, false) + is Demographic -> filter.toggleDemographic(data, false) + is ContentType -> filter.toggleContentType(data, false) + is MangaState -> filter.toggleState(data, false) + is Locale -> filter.setLocale(null) + is Int -> filter.setYear(YEAR_UNKNOWN) + is IntRange -> filter.setYearRange(YEAR_UNKNOWN, YEAR_UNKNOWN) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt index c29c0eeec..e011beaa7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt @@ -3,12 +3,15 @@ package org.koitharu.kotatsu.filter.ui import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.filter.ui.model.FilterProperty +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.search.domain.MangaSearchRepository import javax.inject.Inject import com.google.android.material.R as materialR @@ -18,15 +21,14 @@ class FilterHeaderProducer @Inject constructor( ) { fun observeHeader(filterCoordinator: FilterCoordinator): Flow { - return combine(filterCoordinator.tags, filterCoordinator.query) { tags, query -> - createChipsList( + return combine(filterCoordinator.tags, filterCoordinator.observe()) { tags, snapshot -> + val chipList = createChipsList( source = filterCoordinator.mangaSource, capabilities = filterCoordinator.capabilities, - property = tags, - query = query, + tagsProperty = tags, + snapshot = snapshot.listFilter, limit = 8, ) - }.combine(filterCoordinator.observe()) { chipList, snapshot -> FilterHeaderModel( chips = chipList, sortOrder = snapshot.sortOrder, @@ -38,20 +40,20 @@ class FilterHeaderProducer @Inject constructor( private suspend fun createChipsList( source: MangaSource, capabilities: MangaListFilterCapabilities, - property: FilterProperty, - query: String?, + tagsProperty: FilterProperty, + snapshot: MangaListFilter, limit: Int, ): List { val result = ArrayDeque(limit + 3) - if (query.isNullOrEmpty() || capabilities.isSearchWithFiltersSupported) { - val selectedTags = property.selectedItems.toMutableSet() + if (snapshot.query.isNullOrEmpty() || capabilities.isSearchWithFiltersSupported) { + val selectedTags = tagsProperty.selectedItems.toMutableSet() var tags = if (selectedTags.isEmpty()) { searchRepository.getTagsSuggestion("", limit, source) } else { searchRepository.getTagsSuggestion(selectedTags).take(limit) } if (tags.size < limit) { - tags = tags + property.availableItems.take(limit - tags.size) + tags = tags + tagsProperty.availableItems.take(limit - tags.size) } if (tags.isEmpty() && selectedTags.isEmpty()) { return emptyList() @@ -77,13 +79,59 @@ class FilterHeaderProducer @Inject constructor( result.addFirst(model) } } - if (!query.isNullOrEmpty()) { + snapshot.locale?.let { result.addFirst( ChipsView.ChipModel( - title = query, + title = it.getDisplayName(it).toTitleCase(it), + icon = R.drawable.ic_language, + isCloseable = true, + data = it, + ), + ) + } + snapshot.types.forEach { + result.addFirst( + ChipsView.ChipModel( + titleResId = it.titleResId, + isCloseable = true, + data = it, + ), + ) + } + snapshot.demographics.forEach { + result.addFirst( + ChipsView.ChipModel( + titleResId = it.titleResId, + isCloseable = true, + data = it, + ), + ) + } + snapshot.contentRating.forEach { + result.addFirst( + ChipsView.ChipModel( + titleResId = it.titleResId, + isCloseable = true, + data = it, + ), + ) + } + snapshot.states.forEach { + result.addFirst( + ChipsView.ChipModel( + titleResId = it.titleResId, + isCloseable = true, + data = it, + ), + ) + } + if (!snapshot.query.isNullOrEmpty()) { + result.addFirst( + ChipsView.ChipModel( + title = snapshot.query, icon = materialR.drawable.abc_ic_search_api_material, isCloseable = true, - data = query, + data = snapshot.query, ), ) } @@ -97,6 +145,5 @@ class FilterHeaderProducer @Inject constructor( private fun moreTagsChip() = ChipsView.ChipModel( titleResId = R.string.more, isDropdown = true, - // icon = materialR.drawable.abc_ic_menu_overflow_material, ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt index dc9115332..53952a129 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt @@ -68,12 +68,20 @@ class FilterSheetFragment : BaseAdaptiveSheet(), filter.year.observe(viewLifecycleOwner, this::onYearChanged) filter.yearRange.observe(viewLifecycleOwner, this::onYearRangeChanged) + binding.layoutGenres.setTitle( + if (filter.capabilities.isMultipleTagsSupported) { + R.string.genres + } else { + R.string.genre + }, + ) binding.spinnerLocale.onItemSelectedListener = this binding.spinnerOriginalLocale.onItemSelectedListener = this binding.spinnerOrder.onItemSelectedListener = this binding.chipsState.onChipClickListener = this binding.chipsTypes.onChipClickListener = this binding.chipsContentRating.onChipClickListener = this + binding.chipsDemographics.onChipClickListener = this binding.chipsGenres.onChipClickListener = this binding.chipsGenresExclude.onChipClickListener = this binding.sliderYear.addOnChangeListener(this::onSliderValueChange) @@ -143,6 +151,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), is ContentType -> filter.toggleContentType(data, !chip.isChecked) is ContentRating -> filter.toggleContentRating(data, !chip.isChecked) + is Demographic -> filter.toggleDemographic(data, !chip.isChecked) null -> TagsCatalogSheet.show(getChildFragmentManager(), chip.parentView?.id == R.id.chips_genresExclude) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1b0ecfaa..00cd1261b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -741,4 +741,5 @@ Start download Save selected manga? This may consume traffic and disk space Save manga + Genre