Fixes batch
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user