Compare commits

...

13 Commits

Author SHA1 Message Date
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
Koitharu
557c2b018a Update version and parsers 2022-07-08 12:33:05 +03:00
Koitharu
3add01d57e Improve reporting of caught exceptions 2022-07-08 10:35:18 +03:00
Koitharu
2ad1ea98f1 Fix crash in SourceSettingsFragment 2022-07-08 10:27:11 +03:00
20 changed files with 278 additions and 144 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 413
versionName '3.4.1'
versionCode 415
versionName '3.4.3'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -76,8 +76,7 @@ afterEvaluate {
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation('com.github.nv95:kotatsu-parsers:8c26f3c790') {
implementation('com.github.nv95:kotatsu-parsers:2d1907569b') {
exclude group: 'org.json', module: 'json'
}

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

@@ -25,7 +25,6 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch
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
@@ -39,7 +38,6 @@ import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
@@ -48,6 +46,8 @@ import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.isReportable
import org.koitharu.kotatsu.utils.ext.report
class DetailsActivity :
BaseActivity<ActivityDetailsBinding>(),
@@ -118,7 +118,7 @@ class DetailsActivity :
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition()
}
e is ParseException || e is IllegalArgumentException || e is IllegalStateException -> {
e.isReportable() -> {
binding.snackbar.show(
messageText = e.getDisplayMessage(resources),
actionId = R.string.report,
@@ -128,7 +128,7 @@ class DetailsActivity :
Snackbar.LENGTH_LONG
},
onActionClick = {
e.sendWithAcra()
e.report("DetailsActivity::onError")
dismiss()
}
)

View File

@@ -7,6 +7,7 @@ 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
@@ -115,7 +116,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 +128,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

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

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

@@ -5,6 +5,7 @@ import android.view.View
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
@@ -74,11 +75,15 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
when {
error is AuthRequiredException -> Unit
ExceptionResolver.canResolve(error) -> {
Snackbar.make(listView, error.getDisplayMessage(resources), Snackbar.LENGTH_INDEFINITE)
.setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
ensureActive()
Snackbar.make(
listView ?: return@onFailure,
error.getDisplayMessage(preference.context.resources),
Snackbar.LENGTH_INDEFINITE,
).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
.show()
}
else -> preference.summary = error.getDisplayMessage(resources)
else -> preference.summary = error.getDisplayMessage(preference.context.resources)
}
error.printStackTraceDebug()
}

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

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

@@ -4,12 +4,14 @@ import android.content.ActivityNotFoundException
import android.content.res.Resources
import okio.FileNotFoundException
import org.acra.ACRA
import org.acra.ktx.sendWithAcra
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga
import java.net.SocketTimeoutException
@@ -26,4 +28,17 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
else -> localizedMessage ?: resources.getString(R.string.error_occurred)
}
fun ACRA.setCurrentManga(manga: Manga?) = errorReporter.putCustomData("manga", manga?.publicUrl.toString())
fun Throwable.isReportable(): Boolean {
if (this !is Exception) {
return true
}
return this is ParseException || this is IllegalArgumentException || this is IllegalStateException
}
fun Throwable.report(message: String?) {
CaughtException(this, message).sendWithAcra()
}
fun ACRA.setCurrentManga(manga: Manga?) = errorReporter.putCustomData("manga", manga?.publicUrl.toString())
private class CaughtException(cause: Throwable, override val message: String?) : RuntimeException(cause)

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