From 1290db4a7c0f3ce5592ab0d88cd64dc0a3c5fca4 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Oct 2024 10:23:49 +0300 Subject: [PATCH] Fixes batch --- app/build.gradle | 8 +-- .../core/parser/MangaLoaderContextImpl.kt | 4 +- .../kotatsu/core/util/ext/Throwable.kt | 12 ++++ .../koitharu/kotatsu/core/zip/ZipOutput.kt | 6 +- .../download/ui/list/DownloadItemAD.kt | 5 ++ .../filter/ui/tags/TagsCatalogViewModel.kt | 67 +++++++++++++++---- .../local/data/LocalMangaRepository.kt | 4 +- .../local/data/index/LocalMangaIndex.kt | 9 ++- .../local/data/index/LocalMangaIndexDao.kt | 3 + .../remotelist/ui/RemoteListFragment.kt | 12 ++-- .../remotelist/ui/RemoteListViewModel.kt | 8 +-- .../kotatsu/tracker/ui/debug/TrackDebugAD.kt | 10 +-- 12 files changed, 106 insertions(+), 42 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 66c8e1af5..1d65bb8a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 674 - versionName = '7.6.1' + versionCode = 675 + versionName = '7.6.2' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -64,7 +64,7 @@ android { } lint { abortOnError true - disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled' + disable 'MissingTranslation', 'PrivateResource', 'SetJavaScriptEnabled' } testOptions { unitTests.includeAndroidResources true @@ -83,7 +83,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:a8df8665ae') { + implementation('com.github.KotatsuApp:kotatsu-parsers:645006fde8') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt index b6fb9f9f3..824aea5d2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt @@ -7,6 +7,7 @@ import android.util.Base64 import android.webkit.WebView import androidx.annotation.MainThread import androidx.core.os.LocaleListCompat +import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -30,6 +31,7 @@ import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.util.map +import org.koitharu.kotatsu.parsers.util.mimeType import java.lang.ref.WeakReference import java.util.Locale import javax.inject.Inject @@ -86,7 +88,7 @@ class MangaLoaderContextImpl @Inject constructor( result.compressTo(it.outputStream()) }.asResponseBody("image/jpeg".toMediaType()) } - } ?: error("Cannot decode bitmap") + } ?: throw ImageDecodeException(response.request.url.toString(), response.mimeType) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt index 85afb772c..41a51ce63 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt @@ -113,6 +113,18 @@ fun Throwable.getDisplayIcon() = when (this) { else -> R.drawable.ic_error_large } +fun Throwable.getCauseUrl(): String? = when (this) { + is ParseException -> url + is NotFoundException -> url + is TooManyRequestExceptions -> url + is CaughtException -> cause?.getCauseUrl() + is CloudFlareBlockedException -> url + is CloudFlareProtectedException -> url + is HttpStatusException -> url + is HttpException -> response.request.url.toString() + else -> null +} + private fun getHttpDisplayMessage(statusCode: Int, resources: Resources): String? = when (statusCode) { 404 -> resources.getString(R.string.not_found_404) in 500..599 -> resources.getString(R.string.server_error, statusCode) 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 88b435350..82378614a 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 @@ -6,6 +6,7 @@ import okio.Closeable import org.koitharu.kotatsu.core.util.ext.withChildren import java.io.File import java.io.FileInputStream +import java.util.concurrent.atomic.AtomicBoolean import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipFile @@ -17,7 +18,7 @@ class ZipOutput( ) : Closeable { private val entryNames = ArraySet() - private var isClosed = false + private val isClosed = AtomicBoolean(false) private val output = ZipOutputStream(file.outputStream()).apply { setLevel(compressionLevel) } @@ -72,9 +73,8 @@ class ZipOutput( } override fun close() { - if (!isClosed) { + if (isClosed.compareAndSet(false, true)) { output.close() - isClosed = true } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt index 274dd0a4d..ec7e77736 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt @@ -124,6 +124,7 @@ fun downloadItemAD( binding.buttonCancel.isVisible = true binding.buttonResume.isVisible = false binding.buttonSkip.isVisible = false + binding.buttonSkipAll.isVisible = false binding.buttonPause.isVisible = false } @@ -147,6 +148,7 @@ fun downloadItemAD( binding.buttonResume.isVisible = item.isPaused binding.buttonResume.setText(if (item.error == null) R.string.resume else R.string.retry) binding.buttonSkip.isVisible = item.isPaused && item.error != null + binding.buttonSkipAll.isVisible = item.isPaused && item.error != null binding.buttonPause.isVisible = item.canPause } @@ -169,6 +171,7 @@ fun downloadItemAD( binding.buttonCancel.isVisible = false binding.buttonResume.isVisible = false binding.buttonSkip.isVisible = false + binding.buttonSkipAll.isVisible = false binding.buttonPause.isVisible = false } @@ -182,6 +185,7 @@ fun downloadItemAD( binding.buttonCancel.isVisible = false binding.buttonResume.isVisible = false binding.buttonSkip.isVisible = false + binding.buttonSkipAll.isVisible = false binding.buttonPause.isVisible = false } @@ -195,6 +199,7 @@ fun downloadItemAD( binding.buttonCancel.isVisible = false binding.buttonResume.isVisible = false binding.buttonSkip.isVisible = false + binding.buttonSkipAll.isVisible = false binding.buttonPause.isVisible = false } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt index 5b83142b1..1cc380bd4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt @@ -10,22 +10,27 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus +import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.model.FilterProperty import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorState +import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaTag @HiltViewModel(assistedFactory = TagsCatalogViewModel.Factory::class) class TagsCatalogViewModel @AssistedInject constructor( @Assisted private val filter: FilterCoordinator, @Assisted private val isExcluded: Boolean, + private val mangaDataRepository: MangaDataRepository, ) : BaseViewModel() { val searchQuery = MutableStateFlow("") @@ -33,23 +38,13 @@ class TagsCatalogViewModel @AssistedInject constructor( private val filterProperty: StateFlow> get() = if (isExcluded) filter.tagsExcluded else filter.tags + @Suppress("RemoveExplicitTypeArguments") private val tags: StateFlow> = combine( filter.getAllTags(), + flow> { emit(emptyList()); emit(mangaDataRepository.findTags(filter.mangaSource)) }, filterProperty.map { it.selectedItems }, - ) { all, selected -> - all.fold( - onSuccess = { - it.map { tag -> - TagCatalogItem( - tag = tag, - isChecked = tag in selected, - ) - } - }, - onFailure = { - listOf(it.toErrorState(false)) - }, - ) + ) { available, cached, selected -> + buildList(available, cached, selected) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) val content = combine(tags, searchQuery) { raw, query -> @@ -66,6 +61,50 @@ class TagsCatalogViewModel @AssistedInject constructor( } } + private fun buildList( + available: Result>, + cached: Collection, + selected: Set, + ): List { + val capacity = (available.getOrNull()?.size ?: 1) + cached.size + val result = ArrayList(capacity) + val added = HashSet(capacity) + available.getOrNull()?.forEach { tag -> + if (added.add(tag.title)) { + result.add( + TagCatalogItem( + tag = tag, + isChecked = tag in selected, + ), + ) + } + } + cached.forEach { tag -> + if (added.add(tag.title)) { + result.add( + TagCatalogItem( + tag = tag, + isChecked = tag in selected, + ), + ) + } + } + if (result.isNotEmpty()) { + val locale = (filter.mangaSource as? MangaParserSource)?.locale + result.sortWith(compareBy(TagTitleComparator(locale)) { (it as TagCatalogItem).tag }) + } + available.exceptionOrNull()?.let { error -> + result.add( + if (result.isEmpty()) { + error.toErrorState(canRetry = false) + } else { + error.toErrorFooter() + }, + ) + } + return result + } + @AssistedFactory interface Factory { fun create(filter: FilterCoordinator, isExcludeTag: Boolean): TagsCatalogViewModel diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index 7a3206e44..aa9cb07a2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -76,7 +76,9 @@ class LocalMangaRepository @Inject constructor( } override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = localMangaIndex.getAvailableTags().mapToSet { MangaTag(title = it, key = it, source = source) }, + availableTags = localMangaIndex.getAvailableTags( + skipNsfw = settings.isNsfwContentDisabled, + ).mapToSet { MangaTag(title = it, key = it, source = source) }, availableContentRating = if (!settings.isNsfwContentDisabled) { EnumSet.of(ContentRating.SAFE, ContentRating.ADULT) } else { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt index e7b6757d6..53caf3c53 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt @@ -83,8 +83,13 @@ class LocalMangaIndex @Inject constructor( db.getLocalMangaIndexDao().delete(mangaId) } - suspend fun getAvailableTags(): List { - return db.getLocalMangaIndexDao().findTags() + suspend fun getAvailableTags(skipNsfw: Boolean): List { + val dao = db.getLocalMangaIndexDao() + return if (skipNsfw) { + dao.findTags(isNsfw = false) + } else { + dao.findTags() + } } private suspend fun upsert(manga: LocalManga) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt index 8b1b6f1b0..21038c6ae 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt @@ -13,6 +13,9 @@ interface LocalMangaIndexDao { @Query("SELECT title FROM local_index LEFT JOIN manga_tags ON manga_tags.manga_id = local_index.manga_id LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id WHERE title IS NOT NULL GROUP BY title") suspend fun findTags(): List + @Query("SELECT title FROM local_index LEFT JOIN manga_tags ON manga_tags.manga_id = local_index.manga_id LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id WHERE (SELECT nsfw FROM manga WHERE manga.manga_id = local_index.manga_id) = :isNsfw AND title IS NOT NULL GROUP BY title") + suspend fun findTags(isNsfw: Boolean): List + @Upsert suspend fun upsert(entity: LocalMangaIndexEntity) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 41257db6f..d07dedd78 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.getCauseUrl import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.withArgs @@ -72,24 +73,23 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner { if (filterCoordinator.isFilterApplied) { filterCoordinator.reset() } else { - openInBrowser() + openInBrowser(null) } } override fun onSecondaryErrorActionClick(error: Throwable) { - openInBrowser() + openInBrowser(error.getCauseUrl()) } - private fun openInBrowser() { - val browserUrl = viewModel.browserUrl - if (browserUrl.isNullOrEmpty()) { + private fun openInBrowser(url: String?) { + if (url.isNullOrEmpty()) { Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT) .show() } else { startActivity( BrowserActivity.newIntent( requireContext(), - browserUrl, + url, viewModel.source, viewModel.source.getTitle(requireContext()), ), diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index c3b948f18..b397f429a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -21,10 +21,10 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.distinctById import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.getCauseUrl import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.sizeOrZero import org.koitharu.kotatsu.download.ui.worker.DownloadWorker @@ -39,7 +39,6 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorState -import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.Manga import javax.inject.Inject @@ -68,9 +67,6 @@ open class RemoteListViewModel @Inject constructor( private var loadingJob: Job? = null private var randomJob: Job? = null - val browserUrl: String? - get() = (repository as? ParserMangaRepository)?.domain?.let { "https://$it" } - override val content = combine( mangaList.map { it?.skipNsfwIfNeeded() }, observeListModeWithTriggers(), @@ -82,7 +78,7 @@ open class RemoteListViewModel @Inject constructor( list.isNullOrEmpty() && error != null -> add( error.toErrorState( canRetry = true, - secondaryAction = if (error !is NotFoundException && browserUrl != null) R.string.open_in_browser else 0, + secondaryAction = if (error.getCauseUrl().isNullOrEmpty()) 0 else R.string.open_in_browser, ), ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt index e1a62459a..e305e5f8e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt @@ -43,17 +43,17 @@ fun trackDebugAD( } binding.textViewTitle.text = item.manga.title binding.textViewSummary.text = buildSpannedString { - item.lastCheckTime?.let { - append( + append( + item.lastCheckTime?.let { DateUtils.getRelativeDateTimeString( context, it.toEpochMilli(), DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0, - ), - ) - } + ) + } ?: getString(R.string.never), + ) if (item.lastResult == TrackEntity.RESULT_FAILED) { append(" - ") bold {