Compare commits

...

16 Commits

Author SHA1 Message Date
Koitharu
2d4c1b751e Update dependencies 2022-07-14 14:48:26 +03:00
Koitharu
91b17ef4a2 Fix global search parallelism 2022-07-14 14:23:04 +03:00
Koitharu
9b748f7334 Update parsers and version 2022-07-14 14:12:47 +03:00
Koitharu
2deaed2067 Optimize image loading in lists 2022-07-13 14:48:33 +03:00
Koitharu
fb608ed30a Fix findProgress function not suspend 2022-07-12 12:49:49 +03:00
Koitharu
8e43afe408 Prohibit empty names in favourite categories 2022-07-12 12:02:49 +03:00
Koitharu
73df680214 Add domain validation in source settings 2022-07-12 11:13:33 +03:00
Koitharu
fa4aa154a3 Update parsers and version 2022-07-12 10:53:23 +03:00
Koitharu
cf7cdbc41b Fix CloudFlare resolve url 2022-07-12 10:46:45 +03:00
Koitharu
c2561a1de0 Fix CompositeMutex concurrent locking 2022-07-12 10:46:45 +03:00
Koitharu
a36abe0272 Fix missed git conflicts 2022-07-12 10:46:44 +03:00
Koitharu
5b10d697f6 Fix ReadActivity error reporting 2022-07-12 10:46:44 +03:00
Koitharu
e0f07ccc3b Update issue templates 2022-07-12 10:43:38 +03:00
DevCoz
938ea8fb73 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.7% (316 of 320 strings)

Co-authored-by: DevCoz <qq851428876@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2022-07-12 10:43:28 +03:00
Sergio Varela
ea6a338128 Translated using Weblate (Spanish)
Currently translated at 100.0% (320 of 320 strings)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-07-12 10:43:28 +03:00
Koitharu
ce3a668103 Fix category edit activity recreation 2022-07-09 16:59:12 +03:00
31 changed files with 404 additions and 322 deletions

View File

@@ -2,4 +2,4 @@ blank_issues_enabled: false
contact_links:
- name: ⚠️ Source issue
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
about: Issues and requests for sources should be opened in the kotatsu-parsers repository instead
about: If you have troubles with a manga parser or want to propose new manga source, please open an issue in the kotatsu-parsers repository instead

64
.github/ISSUE_TEMPLATE/report_bug.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: 🐞 Bug report
description: Report a bug in Kotatsu
labels: [bug]
body:
- type: textarea
id: summary
attributes:
label: Brief summary
description: Please describe, what went wrong
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: Steps to reproduce
description: Please provide a way to reproduce this issue. Screenshots or videos can be very helpful
placeholder: |
Example:
1. First step
2. Second step
3. Issue here
validations:
required: false
- type: input
id: kotatsu-version
attributes:
label: Kotatsu version
description: You can find your Kotatsu version in **Settings → About**.
placeholder: |
Example: "3.3"
validations:
required: true
- type: input
id: android-version
attributes:
label: Android version
description: You can find this somewhere in your Android settings.
placeholder: |
Example: "12.0"
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: List your device and model.
placeholder: |
Example: "LG Nexus 5X"
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true

View File

@@ -1,91 +0,0 @@
name: 🐞 Issue report
description: Report an issue in Kotatsu
labels: [bug]
body:
- type: textarea
id: reproduce-steps
attributes:
label: Steps to reproduce
description: Provide an example of the issue.
placeholder: |
Example:
1. First step
2. Second step
3. Issue here
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: Explain what you should expect to happen.
placeholder: |
Example:
"This should happen..."
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: Explain what actually happens.
placeholder: |
Example:
"This happened instead..."
validations:
required: true
- type: input
id: kotatsu-version
attributes:
label: Kotatsu version
description: You can find your Kotatsu version in **Settings → About**.
placeholder: |
Example: "3.3"
validations:
required: true
- type: input
id: android-version
attributes:
label: Android version
description: You can find this somewhere in your Android settings.
placeholder: |
Example: "Android 12"
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: List your device and model.
placeholder: |
Example: "LG Nexus 5X"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new).
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@@ -1,5 +1,5 @@
name: ⭐ Feature request
description: Suggest a feature to improve Kotatsu
description: Suggest a new idea how to improve Kotatsu
labels: [feature request]
body:
@@ -14,23 +14,6 @@ body:
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: input
id: kotatsu-version
attributes:
label: Kotatsu version
description: You can find your Kotatsu version in **Settings → About**.
placeholder: |
Example: "3.3"
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
@@ -38,10 +21,4 @@ body:
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new).
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@@ -14,8 +14,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 32
versionCode 414
versionName '3.4.2'
versionCode 416
versionName '3.4.4'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -76,11 +76,11 @@ afterEvaluate {
}
}
dependencies {
implementation('com.github.nv95:kotatsu-parsers:26d951bc20') {
implementation('com.github.nv95:kotatsu-parsers:6af8cec134') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.activity:activity-ktx:1.5.0'
@@ -116,21 +116,21 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'ch.acra:acra-mail:5.9.3'
implementation 'ch.acra:acra-dialog:5.9.3'
implementation 'ch.acra:acra-mail:5.9.5'
implementation 'ch.acra:acra-dialog:5.9.5'
debugImplementation 'org.jsoup:jsoup:1.15.1'
debugImplementation 'org.jsoup:jsoup:1.15.2'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.3'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.3'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'io.insert-koin:koin-test:3.2.0'
androidTestImplementation 'io.insert-koin:koin-test-junit4:3.2.0'

View File

@@ -2,15 +2,13 @@ package org.koitharu.kotatsu.bookmarks.ui
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
@@ -23,29 +21,24 @@ fun bookmarkListAD(
{ inflater, parent -> ItemBookmarkBinding.inflate(inflater, parent, false) }
) {
var imageRequest: Disposable? = null
val listener = AdapterDelegateClickListenerAdapter(this, clickListener)
binding.root.setOnClickListener(listener)
binding.root.setOnLongClickListener(listener)
bind {
imageRequest?.dispose()
imageRequest = binding.imageViewThumb.newImageRequest(item.imageUrl)
.referer(item.manga.publicUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.allowRgb565(true)
.scale(Scale.FILL)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.imageViewThumb.newImageRequest(item.imageUrl)?.run {
referer(item.manga.publicUrl)
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
allowRgb565(true)
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
}
onViewRecycled {
imageRequest?.dispose()
imageRequest = null
CoilUtils.dispose(binding.imageViewThumb)
binding.imageViewThumb.setImageDrawable(null)
binding.imageViewThumb.disposeImageRequest()
}
}

View File

@@ -17,7 +17,7 @@ class CloudFlareInterceptor : Interceptor {
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
response.closeQuietly()
throw CloudFlareProtectedException(chain.request().url.toString())
throw CloudFlareProtectedException(response.request.url.toString())
}
}
return response

View File

@@ -231,14 +231,13 @@ class DetailsFragment :
CoilUtils.dispose(imageViewCover)
return
}
imageViewCover.newImageRequest(scrobbling.coverUrl)
.crossfade(true)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.scale(Scale.FILL)
.lifecycle(viewLifecycleOwner)
.enqueueWith(coil)
imageViewCover.newImageRequest(scrobbling.coverUrl)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
lifecycle(viewLifecycleOwner)
enqueueWith(coil)
}
textViewTitle.text = scrobbling.title
textViewTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, scrobbling.scrobbler.iconResId, 0)
ratingBar.rating = scrobbling.rating * ratingBar.numStars

View File

@@ -27,13 +27,14 @@ fun downloadItemAD(
bind {
job?.cancel()
job = item.progressAsFlow().onFirst { state ->
binding.imageViewCover.newImageRequest(state.manga.coverUrl)
.referer(state.manga.publicUrl)
.placeholder(state.cover)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.allowRgb565(true)
.enqueueWith(coil)
binding.imageViewCover.newImageRequest(state.manga.coverUrl)?.run {
referer(state.manga.publicUrl)
placeholder(state.cover)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
}.onEach { state ->
binding.textViewTitle.text = state.manga.title
when (state) {

View File

@@ -3,10 +3,13 @@ package org.koitharu.kotatsu.favourites.ui.categories.edit
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Filter
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
@@ -23,7 +26,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class FavouritesCategoryEditActivity : BaseActivity<ActivityCategoryEditBinding>(), AdapterView.OnItemClickListener,
View.OnClickListener {
View.OnClickListener, TextWatcher {
private val viewModel by viewModel<FavouritesCategoryEditViewModel> {
parametersOf(intent.getLongExtra(EXTRA_ID, NO_ID))
@@ -39,6 +42,8 @@ class FavouritesCategoryEditActivity : BaseActivity<ActivityCategoryEditBinding>
}
initSortSpinner()
binding.buttonDone.setOnClickListener(this)
binding.editName.addTextChangedListener(this)
afterTextChanged(binding.editName.text)
viewModel.onSaved.observe(this) { finishAfterTransition() }
viewModel.category.observe(this, ::onCategoryChanged)
@@ -65,13 +70,21 @@ class FavouritesCategoryEditActivity : BaseActivity<ActivityCategoryEditBinding>
override fun onClick(v: View) {
when (v.id) {
R.id.button_done -> viewModel.save(
title = binding.editName.text?.toString().orEmpty(),
title = binding.editName.text?.toString()?.trim().orEmpty(),
sortOrder = getSelectedSortOrder(),
isTrackerEnabled = binding.switchTracker.isChecked,
)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
binding.buttonDone.isEnabled = !s.isNullOrBlank()
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.scrollView.updatePadding(
left = insets.left,
@@ -115,7 +128,7 @@ class FavouritesCategoryEditActivity : BaseActivity<ActivityCategoryEditBinding>
private fun initSortSpinner() {
val entries = CategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) }
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, entries)
val adapter = SortAdapter(this, entries)
binding.editSort.setAdapter(adapter)
binding.editSort.onItemClickListener = this
}
@@ -127,6 +140,19 @@ class FavouritesCategoryEditActivity : BaseActivity<ActivityCategoryEditBinding>
return CategoriesActivity.SORT_ORDERS.getOrNull(index) ?: SortOrder.NEWEST
}
private class SortAdapter(
context: Context,
entries: List<String>,
) : ArrayAdapter<String>(context, android.R.layout.simple_spinner_dropdown_item, entries) {
override fun getFilter(): Filter = EmptyFilter
private object EmptyFilter : Filter() {
override fun performFiltering(constraint: CharSequence?) = FilterResults()
override fun publishResults(constraint: CharSequence?, results: FilterResults?) = Unit
}
}
companion object {
private const val EXTRA_ID = "id"

View File

@@ -42,6 +42,7 @@ class FavouritesCategoryEditViewModel(
isTrackerEnabled: Boolean,
) {
launchLoadingJob {
check(title.isNotEmpty())
if (categoryId == NO_ID) {
repository.createCategory(title, sortOrder, isTrackerEnabled)
} else {

View File

@@ -46,7 +46,7 @@ abstract class HistoryDao {
abstract fun observeCount(): Flow<Int>
@Query("SELECT percent FROM history WHERE manga_id = :id")
abstract fun findProgress(id: Long): Float?
abstract suspend fun findProgress(id: Long): Float?
@Query("DELETE FROM history")
abstract suspend fun clear()

View File

@@ -3,9 +3,6 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
@@ -16,6 +13,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.search.ui.multi.adapter.ItemSizeResolver
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
@@ -29,7 +27,6 @@ fun mangaGridItemAD(
{ inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) }
) {
var imageRequest: Disposable? = null
var badge: BadgeDrawable? = null
itemView.setOnClickListener {
@@ -47,16 +44,15 @@ fun mangaGridItemAD(
bind { payloads ->
binding.textViewTitle.text = item.title
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads)
imageRequest?.dispose()
imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl)
.referer(item.manga.publicUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.allowRgb565(true)
.scale(Scale.FILL)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
referer(item.manga.publicUrl)
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
allowRgb565(true)
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
badge = itemView.bindBadge(badge, item.counter)
}
@@ -64,9 +60,6 @@ fun mangaGridItemAD(
itemView.clearBadge(badge)
binding.progressView.percent = PROGRESS_NONE
badge = null
imageRequest?.dispose()
imageRequest = null
CoilUtils.dispose(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -2,9 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils
import org.koitharu.kotatsu.utils.ext.*
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
@@ -14,10 +12,6 @@ import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun mangaListDetailedItemAD(
coil: ImageLoader,
@@ -27,7 +21,6 @@ fun mangaListDetailedItemAD(
{ inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) }
) {
var imageRequest: Disposable? = null
var badge: BadgeDrawable? = null
itemView.setOnClickListener {
@@ -38,19 +31,18 @@ fun mangaListDetailedItemAD(
}
bind { payloads ->
imageRequest?.dispose()
binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.subtitle
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads)
imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl)
.referer(item.manga.publicUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.scale(Scale.FILL)
.allowRgb565(true)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
referer(item.manga.publicUrl)
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
allowRgb565(true)
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
binding.textViewRating.textAndVisible = item.rating
binding.textViewTags.text = item.tags
itemView.bindBadge(badge, item.counter)
@@ -60,9 +52,6 @@ fun mangaListDetailedItemAD(
itemView.clearBadge(badge)
binding.progressView.percent = PROGRESS_NONE
badge = null
imageRequest?.dispose()
imageRequest = null
CoilUtils.dispose(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -2,9 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils
import org.koitharu.kotatsu.utils.ext.*
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
@@ -13,10 +11,6 @@ import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun mangaListItemAD(
coil: ImageLoader,
@@ -26,7 +20,6 @@ fun mangaListItemAD(
{ inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) }
) {
var imageRequest: Disposable? = null
var badge: BadgeDrawable? = null
itemView.setOnClickListener {
@@ -37,27 +30,23 @@ fun mangaListItemAD(
}
bind {
imageRequest?.dispose()
binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.subtitle
imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl)
.referer(item.manga.publicUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.scale(Scale.FILL)
.allowRgb565(true)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
referer(item.manga.publicUrl)
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
allowRgb565(true)
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
itemView.bindBadge(badge, item.counter)
}
onViewRecycled {
itemView.clearBadge(badge)
badge = null
imageRequest?.dispose()
imageRequest = null
CoilUtils.dispose(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.acra.ktx.sendWithAcra
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
@@ -47,10 +46,7 @@ import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasGlobalPoint
import org.koitharu.kotatsu.utils.ext.observeWithPrevious
import org.koitharu.kotatsu.utils.ext.postDelayed
import org.koitharu.kotatsu.utils.ext.*
import java.util.concurrent.TimeUnit
class ReaderActivity :
@@ -374,7 +370,7 @@ class ReaderActivity :
if (ExceptionResolver.canResolve(exception)) {
tryResolve(exception)
} else {
exception.sendWithAcra()
exception.report("ReaderActivity::onError")
}
} else {
onCancel(dialog)

View File

@@ -2,15 +2,13 @@ package org.koitharu.kotatsu.scrobbling.ui.selector.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
@@ -23,30 +21,24 @@ fun shikimoriMangaAD(
{ inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) }
) {
var imageRequest: Disposable? = null
itemView.setOnClickListener {
clickListener.onItemClick(item, it)
}
bind {
imageRequest?.dispose()
binding.textViewTitle.text = item.name
binding.textViewSubtitle.textAndVisible = item.altName
imageRequest = binding.imageViewCover.newImageRequest(item.cover)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.scale(Scale.FILL)
.allowRgb565(true)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.imageViewCover.newImageRequest(item.cover)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
allowRgb565(true)
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
}
onViewRecycled {
imageRequest?.dispose()
imageRequest = null
CoilUtils.dispose(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -89,26 +89,25 @@ class MultiSearchViewModel(
}
}
private suspend fun searchImpl(q: String) {
private suspend fun searchImpl(q: String) = coroutineScope {
val sources = settings.getMangaSources(includeHidden = false)
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val deferredList = coroutineScope {
sources.map { source ->
async(dispatcher) {
runCatching {
val list = MangaRepository(source).getList(offset = 0, query = q)
.toUi(ListMode.GRID)
if (list.isNotEmpty()) {
MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list)
} else {
null
}
}.onFailure {
it.printStackTraceDebug()
val deferredList = sources.map { source ->
async(dispatcher) {
runCatching {
val list = MangaRepository(source).getList(offset = 0, query = q)
.toUi(ListMode.GRID)
if (list.isNotEmpty()) {
MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list)
} else {
null
}
}.onFailure {
it.printStackTraceDebug()
}
}
}
val errors = ArrayList<Throwable>()
for (deferred in deferredList) {
deferred.await()
@@ -120,13 +119,12 @@ class MultiSearchViewModel(
errors.add(it)
}
}
if (listData.value.isNotEmpty()) {
return
}
when (errors.size) {
0 -> Unit
1 -> throw errors[0]
else -> throw CompositeException(errors)
if (listData.value.isEmpty()) {
when (errors.size) {
0 -> Unit
1 -> throw errors[0]
else -> throw CompositeException(errors)
}
}
}
}

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import coil.request.Disposable
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -16,6 +15,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
@@ -52,27 +52,24 @@ private fun searchSuggestionMangaGridAD(
{ layoutInflater, parent -> ItemSearchSuggestionMangaGridBinding.inflate(layoutInflater, parent, false) }
) {
var imageRequest: Disposable? = null
itemView.setOnClickListener {
listener.onMangaClick(item)
}
bind {
imageRequest?.dispose()
imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.allowRgb565(true)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
allowRgb565(true)
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
binding.textViewTitle.text = item.title
}
onViewRecycled {
imageRequest?.dispose()
binding.imageViewCover.setImageDrawable(null)
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.settings
import okhttp3.internal.toCanonicalHost
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.EditTextValidator
class DomainValidator : EditTextValidator() {
override fun validate(text: String): ValidationResult {
if (text.isBlank()) {
return ValidationResult.Success
}
val host = text.trim().toCanonicalHost()
return if (host == null) {
ValidationResult.Failed(context.getString(R.string.invalid_domain_message))
} else {
ValidationResult.Success
}
}
}

View File

@@ -28,6 +28,7 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
EditTextBindListener(
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI,
hint = key.defaultValue,
validator = DomainValidator(),
)
)
setTitle(R.string.domain)

View File

@@ -6,8 +6,6 @@ import android.view.View
import android.widget.CompoundButton
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
@@ -16,7 +14,9 @@ import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun sourceConfigHeaderDelegate() =
@@ -54,8 +54,6 @@ fun sourceConfigItemDelegate(
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable }
) {
var imageRequest: Disposable? = null
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
listener.onItemEnabledChanged(item, isChecked)
}
@@ -64,17 +62,15 @@ fun sourceConfigItemDelegate(
binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled
binding.textViewDescription.textAndVisible = item.summary
imageRequest = ImageRequest.Builder(context)
.data(item.faviconUrl)
.error(R.drawable.ic_favicon_fallback)
.target(binding.imageViewIcon)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.imageViewIcon.newImageRequest(item.faviconUrl)?.run {
error(R.drawable.ic_favicon_fallback)
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
}
onViewRecycled {
imageRequest?.dispose()
imageRequest = null
binding.imageViewIcon.disposeImageRequest()
}
}

View File

@@ -2,14 +2,17 @@ package org.koitharu.kotatsu.settings.utils
import android.widget.EditText
import androidx.preference.EditTextPreference
import org.koitharu.kotatsu.utils.EditTextValidator
class EditTextBindListener(
private val inputType: Int,
private val hint: String
private val hint: String,
private val validator: EditTextValidator?,
) : EditTextPreference.OnBindEditTextListener {
override fun onBindEditText(editText: EditText) {
editText.inputType = inputType
editText.hint = hint
validator?.attachToEditText(editText)
}
}

View File

@@ -2,8 +2,6 @@ package org.koitharu.kotatsu.tracker.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.size.Scale
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@@ -11,6 +9,7 @@ import org.koitharu.kotatsu.databinding.ItemFeedBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
@@ -22,22 +21,19 @@ fun feedItemAD(
{ inflater, parent -> ItemFeedBinding.inflate(inflater, parent, false) }
) {
var imageRequest: Disposable? = null
itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it)
}
bind {
imageRequest?.dispose()
imageRequest = binding.imageViewCover.newImageRequest(item.imageUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.allowRgb565(true)
.scale(Scale.FILL)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.imageViewCover.newImageRequest(item.imageUrl)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
allowRgb565(true)
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
binding.textViewTitle.text = item.title
binding.textViewSummary.text = context.resources.getQuantityString(
R.plurals.new_chapters,
@@ -47,7 +43,6 @@ fun feedItemAD(
}
onViewRecycled {
imageRequest?.dispose()
binding.imageViewCover.setImageDrawable(null)
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.utils
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -32,11 +34,13 @@ class CompositeMutex<T : Any> : Set<T> {
}
suspend fun lock(element: T) {
waitForRemoval(element)
mutex.withLock {
val lastValue = data.put(element, LinkedList<CancellableContinuation<Unit>>())
check(lastValue == null) {
"CompositeMutex is double-locked for $element"
while (currentCoroutineContext().isActive) {
waitForRemoval(element)
mutex.withLock {
if (data[element] == null) {
data[element] = LinkedList<CancellableContinuation<Unit>>()
return
}
}
}
}

View File

@@ -0,0 +1,54 @@
package org.koitharu.kotatsu.utils
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
import androidx.annotation.CallSuper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import java.lang.ref.WeakReference
abstract class EditTextValidator : TextWatcher {
private var editTextRef: WeakReference<EditText>? = null
protected val context: Context
get() = checkNotNull(editTextRef?.get()?.context) {
"EditTextValidator is not attached to EditText"
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
@CallSuper
override fun afterTextChanged(s: Editable?) {
val editText = editTextRef?.get() ?: return
val newText = s?.toString().orEmpty()
val result = runCatching {
validate(newText)
}.getOrElse { e ->
ValidationResult.Failed(e.getDisplayMessage(editText.resources))
}
editText.error = when (result) {
is ValidationResult.Failed -> result.message
ValidationResult.Success -> null
}
}
fun attachToEditText(editText: EditText) {
editTextRef = WeakReference(editText)
editText.removeTextChangedListener(this)
editText.addTextChangedListener(this)
afterTextChanged(editText.text)
}
abstract fun validate(text: String): ValidationResult
sealed class ValidationResult {
object Success : ValidationResult()
class Failed(val message: CharSequence) : ValidationResult()
}
}

View File

@@ -7,14 +7,26 @@ import coil.request.ErrorResult
import coil.request.ImageRequest
import coil.request.ImageResult
import coil.request.SuccessResult
import coil.util.CoilUtils
import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener
fun ImageView.newImageRequest(url: String?) = ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.target(this)
fun ImageView.newImageRequest(url: Any?): ImageRequest.Builder? {
val current = CoilUtils.result(this)
if (current != null && current.request.data == url) {
return null
}
return ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.target(this)
}
fun ImageView.disposeImageRequest() {
CoilUtils.dispose(this)
setImageDrawable(null)
}
fun ImageRequest.Builder.enqueueWith(loader: ImageLoader) = loader.enqueue(build())

View File

@@ -302,4 +302,19 @@
<string name="appwidget_shelf_description">Mangas de tus favoritos</string>
<string name="appwidget_recent_description">Sus mangas recientemente leídos</string>
<string name="logout">Cerrar sesión</string>
<string name="status_planned">Planeado</string>
<string name="status_reading">Lectura</string>
<string name="status_dropped">Abandonado</string>
<string name="status_on_hold">En espera</string>
<string name="report">Informar</string>
<string name="tracking">Seguimiento</string>
<string name="disable_battery_optimization_summary">Ayuda con las comprobaciones de las actualizaciones en segundo plano</string>
<string name="status_re_reading">Relectura</string>
<string name="status_completed">Completado</string>
<string name="show_reading_indicators">Mostrar indicadores de progreso en la lectura</string>
<string name="data_deletion">Eliminación de datos</string>
<string name="show_reading_indicators_summary">Mostrar porcentaje de lectura en el historial y en los favoritos</string>
<string name="exclude_nsfw_from_history_summary">El manga marcado como NSFW nunca se añadirá al historial y no se guardará tu progreso</string>
<string name="clear_cookies_summary">Puede ayudar en caso de algunos problemas. Todas las autorizaciones serán invalidadas</string>
<string name="show_all">Mostrar todo</string>
</resources>

View File

@@ -31,7 +31,7 @@
<string name="clear_cookies">清除cookies</string>
<string name="chapters_checking_progress">检查新的章节: %1$d/%2$d</string>
<string name="error_empty_name">你必须输入一个名称</string>
<string name="new_sources_text">有了新的漫画来源</string>
<string name="new_sources_text">新的漫画来源</string>
<string name="suggestions_summary">根据你的喜好推荐漫画</string>
<string name="suggestions_info">所有的数据都在这个设备上进行本地分析. 您的个人数据不会被转移到任何服务机构</string>
<string name="never">从不</string>
@@ -284,7 +284,7 @@
<string name="bookmark_remove">删除书签</string>
<string name="bookmarks">书签</string>
<string name="bookmark_removed">删除书签</string>
<string name="bookmark_added">书签</string>
<string name="bookmark_added">加书签</string>
<string name="undo">撤销</string>
<string name="removed_from_history">从历史中删除</string>
<string name="dns_over_https">DNS over HTTPS</string>
@@ -295,4 +295,22 @@
<string name="disable_battery_optimization_summary">帮助进行背景更新检查</string>
<string name="crash_text">出了点问题. 请向开发人员提交一份错误报告以帮助我们修复它.</string>
<string name="send">发送</string>
<string name="disable_all">全部禁用</string>
<string name="status_planned">计划</string>
<string name="status_on_hold">暂停</string>
<string name="report">报告</string>
<string name="tracking">追踪</string>
<string name="logout">注销</string>
<string name="status_reading">阅读</string>
<string name="status_re_reading">重读</string>
<string name="status_completed">完成</string>
<string name="use_fingerprint">使用指纹</string>
<string name="appwidget_shelf_description">你喜欢的漫画</string>
<string name="appwidget_recent_description">您最近阅读的漫画</string>
<string name="show_reading_indicators_summary">在历史和收藏夹中显示阅读百分比</string>
<string name="show_reading_indicators">显示阅读进度指标</string>
<string name="data_deletion">数据删除</string>
<string name="exclude_nsfw_from_history_summary">标记为NSFW的漫画将永远不会被添加到历史中你的进度也不会被保存</string>
<string name="clear_cookies_summary">可以在出现一些问题时提供帮助. 所有授权将被视为无效</string>
<string name="show_all">显示全部</string>
</resources>

View File

@@ -320,4 +320,5 @@
<string name="exclude_nsfw_from_history_summary">Manga marked as NSFW will never added to the history and your progress will not be saved</string>
<string name="clear_cookies_summary">Can help in case of some issues. All authorizations will be invalidated</string>
<string name="show_all">Show all</string>
<string name="invalid_domain_message">Invalid domain</string>
</resources>

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.yield
import org.junit.Assert.assertNull
import org.junit.Test
class CompositeMutexTest {
@Test
fun testSingleLock() = runTest {
val mutex = CompositeMutex<Int>()
mutex.lock(1)
mutex.lock(2)
mutex.unlock(1)
assert(mutex.size == 1)
mutex.unlock(2)
assert(mutex.isEmpty())
}
@Test
fun testDoubleLock() = runTest {
val mutex = CompositeMutex<Int>()
repeat(2) {
launch(Dispatchers.Default) {
mutex.lock(1)
}
}
yield()
mutex.unlock(1)
val tryLock = withTimeoutOrNull(1000) {
mutex.lock(1)
}
assertNull(tryLock)
}
}