Fixes batch

This commit is contained in:
Koitharu
2024-10-04 10:23:49 +03:00
parent 1f1309d934
commit 1290db4a7c
12 changed files with 106 additions and 42 deletions

View File

@@ -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'
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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<String>()
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
}
}

View File

@@ -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
}
}

View File

@@ -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<FilterProperty<MangaTag>>
get() = if (isExcluded) filter.tagsExcluded else filter.tags
@Suppress("RemoveExplicitTypeArguments")
private val tags: StateFlow<List<ListModel>> = combine(
filter.getAllTags(),
flow<Collection<MangaTag>> { 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<List<MangaTag>>,
cached: Collection<MangaTag>,
selected: Set<MangaTag>,
): List<ListModel> {
val capacity = (available.getOrNull()?.size ?: 1) + cached.size
val result = ArrayList<ListModel>(capacity)
val added = HashSet<String>(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

View File

@@ -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 {

View File

@@ -83,8 +83,13 @@ class LocalMangaIndex @Inject constructor(
db.getLocalMangaIndexDao().delete(mangaId)
}
suspend fun getAvailableTags(): List<String> {
return db.getLocalMangaIndexDao().findTags()
suspend fun getAvailableTags(skipNsfw: Boolean): List<String> {
val dao = db.getLocalMangaIndexDao()
return if (skipNsfw) {
dao.findTags(isNsfw = false)
} else {
dao.findTags()
}
}
private suspend fun upsert(manga: LocalManga) {

View File

@@ -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<String>
@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<String>
@Upsert
suspend fun upsert(entity: LocalMangaIndexEntity)

View File

@@ -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()),
),

View File

@@ -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,
),
)

View File

@@ -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 {