Compare commits

...

25 Commits
v3.4 ... v3.4.4

Author SHA1 Message Date
2d4c1b751e Update dependencies 2022-07-14 14:48:26 +03:00
91b17ef4a2 Fix global search parallelism 2022-07-14 14:23:04 +03:00
9b748f7334 Update parsers and version 2022-07-14 14:12:47 +03:00
2deaed2067 Optimize image loading in lists 2022-07-13 14:48:33 +03:00
fb608ed30a Fix findProgress function not suspend 2022-07-12 12:49:49 +03:00
8e43afe408 Prohibit empty names in favourite categories 2022-07-12 12:02:49 +03:00
73df680214 Add domain validation in source settings 2022-07-12 11:13:33 +03:00
fa4aa154a3 Update parsers and version 2022-07-12 10:53:23 +03:00
cf7cdbc41b Fix CloudFlare resolve url 2022-07-12 10:46:45 +03:00
c2561a1de0 Fix CompositeMutex concurrent locking 2022-07-12 10:46:45 +03:00
a36abe0272 Fix missed git conflicts 2022-07-12 10:46:44 +03:00
5b10d697f6 Fix ReadActivity error reporting 2022-07-12 10:46:44 +03:00
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
ce3a668103 Fix category edit activity recreation 2022-07-09 16:59:12 +03:00
557c2b018a Update version and parsers 2022-07-08 12:33:05 +03:00
3add01d57e Improve reporting of caught exceptions 2022-07-08 10:35:18 +03:00
2ad1ea98f1 Fix crash in SourceSettingsFragment 2022-07-08 10:27:11 +03:00
3121532217 Translated using Weblate (Russian)
Currently translated at 100.0% (320 of 320 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2022-07-07 11:35:56 +03:00
20ac12ca0d Update readme and metadata 2022-07-07 11:06:20 +03:00
f0b222140e Update parsers 2022-07-07 10:17:58 +03:00
2a35ca6094 Fix landscape details view #197 2022-07-07 09:14:50 +03:00
93f9636916 Improve webtoon reader performance 2022-07-06 19:43:43 +03:00
2c24aba558 Refactor update checker 2022-07-06 16:15:25 +03:00
45 changed files with 892 additions and 717 deletions

View File

@@ -2,4 +2,4 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: ⚠️ Source issue - name: ⚠️ Source issue
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new 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 name: ⭐ Feature request
description: Suggest a feature to improve Kotatsu description: Suggest a new idea how to improve Kotatsu
labels: [feature request] labels: [feature request]
body: body:
@@ -14,23 +14,6 @@ body:
validations: validations:
required: true 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 - type: checkboxes
id: acknowledgements id: acknowledgements
attributes: attributes:
@@ -38,10 +21,4 @@ body:
description: Read this carefully, we will close and ignore your issue if you skimmed through this. description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options: options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. - 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 required: true

View File

@@ -10,9 +10,9 @@ Kotatsu is a free and open source manga reader for Android.
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/org.koitharu.kotatsu) height="80">](https://f-droid.org/packages/org.koitharu.kotatsu)
Download APK from GitHub Releases: Download APK directly from GitHub:
- [Latest release](https://github.com/KotatsuApp/Kotatsu/releases/latest) - **[Latest release](https://github.com/KotatsuApp/Kotatsu/releases/latest)**
### Main Features ### Main Features
@@ -24,8 +24,8 @@ Download APK from GitHub Releases:
* Tablet-optimized material design UI * Tablet-optimized material design UI
* Standard and Webtoon-optimized reader * Standard and Webtoon-optimized reader
* Notifications about new chapters with updates feed * Notifications about new chapters with updates feed
* Available in multiple languages * Shikimori integration (manga tracking)
* Password protect access to the app * Password/fingerprint protect access to the app
### Screenshots ### Screenshots
@@ -38,23 +38,20 @@ Download APK from GitHub Releases:
### Localization ### Localization
<a href="https://hosted.weblate.org/engage/kotatsu/"> [<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status">](https://hosted.weblate.org/engage/kotatsu/)
<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status" />
</a>
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages, Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
please head over to the Weblate <a href="https://hosted.weblate.org/engage/kotatsu/">project page</a> please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
### License ### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
Kotatsu is Free Software: You can use, study share and improve it at your You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications
will. Specifically you can redistribute and/or modify it under the terms of the to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build &
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as install instructions.
published by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
### Disclaimer ### DMCA disclaimer
The developers of this application does not have any affiliation with the content providers available. The developers of this application does not have any affiliation with the content available in the app.
It is collecting from the sources freely available through any web browser.

View File

@@ -14,8 +14,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 32 targetSdkVersion 32
versionCode 412 versionCode 416
versionName '3.4' versionName '3.4.4'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -76,12 +76,11 @@ afterEvaluate {
} }
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation('com.github.nv95:kotatsu-parsers:6af8cec134') {
implementation('com.github.nv95:kotatsu-parsers:da3b0ae0cf') {
exclude group: 'org.json', module: 'json' 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.core:core-ktx:1.8.0'
implementation 'androidx.activity:activity-ktx:1.5.0' implementation 'androidx.activity:activity-ktx:1.5.0'
@@ -117,21 +116,21 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'ch.acra:acra-mail:5.9.3' implementation 'ch.acra:acra-mail:5.9.5'
implementation 'ch.acra:acra-dialog:5.9.3' 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' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
testImplementation 'junit:junit:4.13.2' 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:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' 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:3.2.0'
androidTestImplementation 'io.insert-koin:koin-test-junit4: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 androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding 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.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.referer
@@ -23,29 +21,24 @@ fun bookmarkListAD(
{ inflater, parent -> ItemBookmarkBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemBookmarkBinding.inflate(inflater, parent, false) }
) { ) {
var imageRequest: Disposable? = null
val listener = AdapterDelegateClickListenerAdapter(this, clickListener) val listener = AdapterDelegateClickListenerAdapter(this, clickListener)
binding.root.setOnClickListener(listener) binding.root.setOnClickListener(listener)
binding.root.setOnLongClickListener(listener) binding.root.setOnLongClickListener(listener)
bind { bind {
imageRequest?.dispose() binding.imageViewThumb.newImageRequest(item.imageUrl)?.run {
imageRequest = binding.imageViewThumb.newImageRequest(item.imageUrl) referer(item.manga.publicUrl)
.referer(item.manga.publicUrl) placeholder(R.drawable.ic_placeholder)
.placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder) error(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder) allowRgb565(true)
.allowRgb565(true) lifecycle(lifecycleOwner)
.scale(Scale.FILL) enqueueWith(coil)
.lifecycle(lifecycleOwner) }
.enqueueWith(coil)
} }
onViewRecycled { onViewRecycled {
imageRequest?.dispose() binding.imageViewThumb.disposeImageRequest()
imageRequest = null
CoilUtils.dispose(binding.imageViewThumb)
binding.imageViewThumb.setImageDrawable(null)
} }
} }

View File

@@ -17,7 +17,7 @@ class CloudFlareInterceptor : Interceptor {
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) { if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) { if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
response.closeQuietly() response.closeQuietly()
throw CloudFlareProtectedException(chain.request().url.toString()) throw CloudFlareProtectedException(response.request.url.toString())
} }
} }
return response 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.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ktx.sendWithAcra
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf 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.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.download.ui.service.DownloadService 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet 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.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.isReportable
import org.koitharu.kotatsu.utils.ext.report
class DetailsActivity : class DetailsActivity :
BaseActivity<ActivityDetailsBinding>(), BaseActivity<ActivityDetailsBinding>(),
@@ -118,7 +118,7 @@ class DetailsActivity :
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition() finishAfterTransition()
} }
e is ParseException || e is IllegalArgumentException || e is IllegalStateException -> { e.isReportable() -> {
binding.snackbar.show( binding.snackbar.show(
messageText = e.getDisplayMessage(resources), messageText = e.getDisplayMessage(resources),
actionId = R.string.report, actionId = R.string.report,
@@ -128,7 +128,7 @@ class DetailsActivity :
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
}, },
onActionClick = { onActionClick = {
e.sendWithAcra() e.report("DetailsActivity::onError")
dismiss() dismiss()
} }
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.sign
class WebtoonLayoutManager : LinearLayoutManager {
private var scrollDirection: Int = 0
constructor(context: Context) : super(context)
constructor(
context: Context,
orientation: Int,
reverseLayout: Boolean,
) : super(context, orientation, reverseLayout)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int,
) : super(context, attrs, defStyleAttr, defStyleRes)
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State): Int {
scrollDirection = dy.sign
return super.scrollVerticallyBy(dy, recycler, state)
}
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
if (state.hasTargetScrollPosition()) {
super.calculateExtraLayoutSpace(state, extraLayoutSpace)
return
}
val pageSize = height
extraLayoutSpace[0] = if (scrollDirection < 0) pageSize else 0
extraLayoutSpace[1] = if (scrollDirection < 0) 0 else pageSize
}
}

View File

@@ -12,7 +12,6 @@ import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@@ -33,7 +32,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
adapter = webtoonAdapter adapter = webtoonAdapter
doOnCurrentItemChanged(::notifyPageChanged) addOnPageScrollListener(PageScrollListener())
} }
} }
@@ -93,4 +92,12 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
override fun switchPageTo(position: Int, smooth: Boolean) { override fun switchPageTo(position: Int, smooth: Boolean) {
binding.recyclerView.firstVisibleItemPosition = position binding.recyclerView.firstVisibleItemPosition = position
} }
private inner class PageScrollListener : WebtoonRecyclerView.OnPageScrollListener() {
override fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) {
super.onPageChanged(recyclerView, index)
notifyPageChanged(index)
}
}
} }

View File

@@ -2,25 +2,27 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat.TYPE_TOUCH
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
import java.util.*
class WebtoonRecyclerView @JvmOverloads constructor( class WebtoonRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) { ) : RecyclerView(context, attrs, defStyleAttr) {
override fun startNestedScroll(axes: Int) = startNestedScroll(axes, ViewCompat.TYPE_TOUCH) private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null
override fun startNestedScroll(axes: Int, type: Int): Boolean { override fun startNestedScroll(axes: Int) = startNestedScroll(axes, TYPE_TOUCH)
return true
} override fun startNestedScroll(axes: Int, type: Int): Boolean = true
override fun dispatchNestedPreScroll( override fun dispatchNestedPreScroll(
dx: Int, dx: Int,
dy: Int, dy: Int,
consumed: IntArray?, consumed: IntArray?,
offsetInWindow: IntArray? offsetInWindow: IntArray?
) = dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH) ) = dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH)
override fun dispatchNestedPreScroll( override fun dispatchNestedPreScroll(
dx: Int, dx: Int,
@@ -34,6 +36,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
consumed[0] = 0 consumed[0] = 0
consumed[1] = consumedY consumed[1] = consumedY
} }
notifyScrollChanged(dy)
return consumedY != 0 || dy == 0 return consumedY != 0 || dy == 0
} }
@@ -75,4 +78,39 @@ class WebtoonRecyclerView @JvmOverloads constructor(
} }
return 0 return 0
} }
fun addOnPageScrollListener(listener: OnPageScrollListener) {
val list = onPageScrollListeners ?: LinkedList<OnPageScrollListener>().also { onPageScrollListeners = it }
list.add(listener)
}
fun removeOnPageScrollListener(listener: OnPageScrollListener) {
onPageScrollListeners?.remove(listener)
}
private fun notifyScrollChanged(dy: Int) {
val listeners = onPageScrollListeners
if (listeners.isNullOrEmpty()) {
return
}
val centerPosition = findCenterViewPosition()
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
}
abstract class OnPageScrollListener {
private var lastPosition = NO_POSITION
fun dispatchScroll(recyclerView: WebtoonRecyclerView, dy: Int, centerPosition: Int) {
onScroll(recyclerView, dy)
if (centerPosition != NO_POSITION && centerPosition != lastPosition) {
lastPosition = centerPosition
onPageChanged(recyclerView, centerPosition)
}
}
open fun onScroll(recyclerView: WebtoonRecyclerView, dy: Int) = Unit
open fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) = Unit
}
} }

View File

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

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 sources = settings.getMangaSources(includeHidden = false)
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val deferredList = coroutineScope { val deferredList = sources.map { source ->
sources.map { source -> async(dispatcher) {
async(dispatcher) { runCatching {
runCatching { val list = MangaRepository(source).getList(offset = 0, query = q)
val list = MangaRepository(source).getList(offset = 0, query = q) .toUi(ListMode.GRID)
.toUi(ListMode.GRID) if (list.isNotEmpty()) {
if (list.isNotEmpty()) { MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list)
MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list) } else {
} else { null
null
}
}.onFailure {
it.printStackTraceDebug()
} }
}.onFailure {
it.printStackTraceDebug()
} }
} }
} }
val errors = ArrayList<Throwable>() val errors = ArrayList<Throwable>()
for (deferred in deferredList) { for (deferred in deferredList) {
deferred.await() deferred.await()
@@ -120,13 +119,12 @@ class MultiSearchViewModel(
errors.add(it) errors.add(it)
} }
} }
if (listData.value.isNotEmpty()) { if (listData.value.isEmpty()) {
return when (errors.size) {
} 0 -> Unit
when (errors.size) { 1 -> throw errors[0]
0 -> Unit else -> throw CompositeException(errors)
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.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import coil.request.Disposable
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding 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.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback 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.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest
@@ -52,27 +52,24 @@ private fun searchSuggestionMangaGridAD(
{ layoutInflater, parent -> ItemSearchSuggestionMangaGridBinding.inflate(layoutInflater, parent, false) } { layoutInflater, parent -> ItemSearchSuggestionMangaGridBinding.inflate(layoutInflater, parent, false) }
) { ) {
var imageRequest: Disposable? = null
itemView.setOnClickListener { itemView.setOnClickListener {
listener.onMangaClick(item) listener.onMangaClick(item)
} }
bind { bind {
imageRequest?.dispose() binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl) placeholder(R.drawable.ic_placeholder)
.placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder) error(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder) allowRgb565(true)
.allowRgb565(true) lifecycle(lifecycleOwner)
.lifecycle(lifecycleOwner) enqueueWith(coil)
.enqueueWith(coil) }
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
} }
onViewRecycled { onViewRecycled {
imageRequest?.dispose() binding.imageViewCover.disposeImageRequest()
binding.imageViewCover.setImageDrawable(null)
} }
} }

View File

@@ -4,9 +4,9 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.core.net.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -23,9 +23,6 @@ import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -74,7 +71,8 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
.setTitle(R.string.app_update_available) .setTitle(R.string.app_update_available)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.download) { _, _ -> .setPositiveButton(R.string.download) { _, _ ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl))) val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri())
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.open_in_browser)))
} }
.setNegativeButton(R.string.close, null) .setNegativeButton(R.string.close, null)
.setCancelable(false) .setCancelable(false)
@@ -88,42 +86,23 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
private val PERIOD = TimeUnit.HOURS.toMillis(6) private val PERIOD = TimeUnit.HOURS.toMillis(6)
fun isUpdateSupported(context: Context): Boolean { fun isUpdateSupported(context: Context): Boolean {
return getCertificateSHA1Fingerprint(context) == CERT_SHA1 return BuildConfig.DEBUG || getCertificateSHA1Fingerprint(context) == CERT_SHA1
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures") @SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(context: Context): String? { private fun getCertificateSHA1Fingerprint(context: Context): String? = runCatching {
val packageInfo = try { val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
context.packageManager.getPackageInfo( val signatures = requireNotNull(packageInfo?.signatures)
context.packageName, val cert: ByteArray = signatures.first().toByteArray()
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTraceDebug()
return null
}
val signatures = packageInfo?.signatures
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
val input: InputStream = ByteArrayInputStream(cert) val input: InputStream = ByteArrayInputStream(cert)
val c = try { val cf = CertificateFactory.getInstance("X509")
val cf = CertificateFactory.getInstance("X509") val c = cf.generateCertificate(input) as X509Certificate
cf.generateCertificate(input) as X509Certificate val md: MessageDigest = MessageDigest.getInstance("SHA1")
} catch (e: CertificateException) { val publicKey: ByteArray = md.digest(c.encoded)
e.printStackTraceDebug() return publicKey.byte2HexFormatted()
return null }.onFailure { error ->
} error.printStackTraceDebug()
return try { }.getOrNull()
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTraceDebug()
null
} catch (e: CertificateEncodingException) {
e.printStackTraceDebug()
null
}
}
} }
} }

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( EditTextBindListener(
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI, inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI,
hint = key.defaultValue, hint = key.defaultValue,
validator = DomainValidator(),
) )
) )
setTitle(R.string.domain) setTitle(R.string.domain)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.utils package org.koitharu.kotatsu.utils
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -32,11 +34,13 @@ class CompositeMutex<T : Any> : Set<T> {
} }
suspend fun lock(element: T) { suspend fun lock(element: T) {
waitForRemoval(element) while (currentCoroutineContext().isActive) {
mutex.withLock { waitForRemoval(element)
val lastValue = data.put(element, LinkedList<CancellableContinuation<Unit>>()) mutex.withLock {
check(lastValue == null) { if (data[element] == null) {
"CompositeMutex is double-locked for $element" 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.ImageRequest
import coil.request.ImageResult import coil.request.ImageResult
import coil.request.SuccessResult import coil.request.SuccessResult
import coil.util.CoilUtils
import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener
fun ImageView.newImageRequest(url: String?) = ImageRequest.Builder(context) fun ImageView.newImageRequest(url: Any?): ImageRequest.Builder? {
.data(url) val current = CoilUtils.result(this)
.crossfade(true) if (current != null && current.request.data == url) {
.target(this) 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()) fun ImageRequest.Builder.enqueueWith(loader: ImageLoader) = loader.enqueue(build())

View File

@@ -4,12 +4,14 @@ import android.content.ActivityNotFoundException
import android.content.res.Resources import android.content.res.Resources
import okio.FileNotFoundException import okio.FileNotFoundException
import org.acra.ACRA import org.acra.ACRA
import org.acra.ktx.sendWithAcra
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
@@ -26,4 +28,17 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
else -> localizedMessage ?: resources.getString(R.string.error_occurred) 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

@@ -91,22 +91,6 @@ fun View.resetTransformations() {
rotationY = 0f rotationY = 0f
} }
inline fun RecyclerView.doOnCurrentItemChanged(crossinline callback: (Int) -> Unit) {
addOnScrollListener(object : RecyclerView.OnScrollListener() {
private var lastItem = -1
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val item = recyclerView.findCenterViewPosition()
if (item != RecyclerView.NO_POSITION && item != lastItem) {
lastItem = item
callback(item)
}
}
})
}
fun RecyclerView.findCenterViewPosition(): Int { fun RecyclerView.findCenterViewPosition(): Int {
val centerX = width / 2f val centerX = width / 2f
val centerY = height / 2f val centerY = height / 2f

View File

@@ -42,9 +42,8 @@
<LinearLayout <LinearLayout
android:id="@+id/layout_titles" android:id="@+id/layout_titles"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover" app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -151,6 +150,14 @@
</LinearLayout> </LinearLayout>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:barrierMargin="8dp"
app:constraint_referenced_ids="layout_titles,imageView_cover" />
<org.koitharu.kotatsu.base.ui.widgets.ChipsView <org.koitharu.kotatsu.base.ui.widgets.ChipsView
android:id="@+id/chips_tags" android:id="@+id/chips_tags"
android:layout_width="0dp" android:layout_width="0dp"
@@ -162,7 +169,7 @@
app:chipSpacingVertical="6dp" app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layout_titles" /> app:layout_constraintTop_toBottomOf="@+id/barrier_header" />
<TextView <TextView
android:id="@+id/textView_bookmarks" android:id="@+id/textView_bookmarks"
@@ -223,7 +230,7 @@
android:textIsSelectable="true" android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recyclerView_bookmarks" app:layout_constraintTop_toBottomOf="@id/scrobbling_layout"
tools:ignore="UnusedAttribute" tools:ignore="UnusedAttribute"
tools:text="@tools:sample/lorem/random[250]" /> tools:text="@tools:sample/lorem/random[250]" />

View File

@@ -6,4 +6,4 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />

View File

@@ -302,4 +302,19 @@
<string name="appwidget_shelf_description">Mangas de tus favoritos</string> <string name="appwidget_shelf_description">Mangas de tus favoritos</string>
<string name="appwidget_recent_description">Sus mangas recientemente leídos</string> <string name="appwidget_recent_description">Sus mangas recientemente leídos</string>
<string name="logout">Cerrar sesión</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> </resources>

View File

@@ -1,305 +1,320 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="close_menu">Закрыть меню</string> <string name="close_menu">Закрыть меню</string>
<string name="open_menu">Открыть меню</string> <string name="open_menu">Открыть меню</string>
<string name="local_storage">На устройстве</string> <string name="local_storage">На устройстве</string>
<string name="favourites">Избранное</string> <string name="favourites">Избранное</string>
<string name="history">История</string> <string name="history">История</string>
<string name="error_occurred">Произошла ошибка</string> <string name="error_occurred">Произошла ошибка</string>
<string name="network_error">Не удалось подключиться к интернету</string> <string name="network_error">Не удалось подключиться к интернету</string>
<string name="details">Подробности</string> <string name="details">Подробности</string>
<string name="chapters">Главы</string> <string name="chapters">Главы</string>
<string name="list">Список</string> <string name="list">Список</string>
<string name="detailed_list">Подробный список</string> <string name="detailed_list">Подробный список</string>
<string name="grid">Таблица</string> <string name="grid">Таблица</string>
<string name="list_mode">Вид списка</string> <string name="list_mode">Вид списка</string>
<string name="settings">Настройки</string> <string name="settings">Настройки</string>
<string name="remote_sources">Онлайн каталоги</string> <string name="remote_sources">Онлайн каталоги</string>
<string name="loading_">Загрузка…</string> <string name="loading_">Загрузка…</string>
<string name="chapter_d_of_d">Глава %1$d из %2$d</string> <string name="chapter_d_of_d">Глава %1$d из %2$d</string>
<string name="close">Закрыть</string> <string name="close">Закрыть</string>
<string name="try_again">Повторить</string> <string name="try_again">Повторить</string>
<string name="clear_history">Очистить историю</string> <string name="clear_history">Очистить историю</string>
<string name="nothing_found">Ничего не найдено</string> <string name="nothing_found">Ничего не найдено</string>
<string name="history_is_empty">Истории пока нет</string> <string name="history_is_empty">Истории пока нет</string>
<string name="read">Читать</string> <string name="read">Читать</string>
<string name="you_have_not_favourites_yet">Избранного пока нет</string> <string name="you_have_not_favourites_yet">Избранного пока нет</string>
<string name="add_to_favourites">В избранное</string> <string name="add_to_favourites">В избранное</string>
<string name="add_new_category">Новая категория</string> <string name="add_new_category">Новая категория</string>
<string name="add">Добавить</string> <string name="add">Добавить</string>
<string name="enter_category_name">Введите название</string> <string name="enter_category_name">Введите название</string>
<string name="save">Сохранить</string> <string name="save">Сохранить</string>
<string name="share">Поделиться</string> <string name="share">Поделиться</string>
<string name="create_shortcut">Создать ярлык…</string> <string name="create_shortcut">Создать ярлык…</string>
<string name="share_s">Поделиться %s</string> <string name="share_s">Поделиться %s</string>
<string name="search">Поиск</string> <string name="search">Поиск</string>
<string name="search_manga">Поиск манги</string> <string name="search_manga">Поиск манги</string>
<string name="manga_downloading_">Загрузка…</string> <string name="manga_downloading_">Загрузка…</string>
<string name="processing_">Обработка…</string> <string name="processing_">Обработка…</string>
<string name="download_complete">Загружено</string> <string name="download_complete">Загружено</string>
<string name="downloads">Загрузки</string> <string name="downloads">Загрузки</string>
<string name="by_name">Имя</string> <string name="by_name">Имя</string>
<string name="popular">Популярная</string> <string name="popular">Популярная</string>
<string name="updated">Обновлённая</string> <string name="updated">Обновлённая</string>
<string name="newest">Новая</string> <string name="newest">Новая</string>
<string name="by_rating">Рейтинг</string> <string name="by_rating">Рейтинг</string>
<string name="sort_order">Порядок сортировки</string> <string name="sort_order">Порядок сортировки</string>
<string name="filter">Фильтр</string> <string name="filter">Фильтр</string>
<string name="theme">Тема</string> <string name="theme">Тема</string>
<string name="light">Светлая</string> <string name="light">Светлая</string>
<string name="dark">Тёмная</string> <string name="dark">Тёмная</string>
<string name="automatic">Как в системе</string> <string name="automatic">Как в системе</string>
<string name="pages">Страницы</string> <string name="pages">Страницы</string>
<string name="clear">Очистить</string> <string name="clear">Очистить</string>
<string name="text_clear_history_prompt">Очистить всю историю чтения полностью\?</string> <string name="text_clear_history_prompt">Очистить всю историю чтения полностью\?</string>
<string name="remove">Удалить</string> <string name="remove">Удалить</string>
<string name="_s_removed_from_history">«%s» удалено из истории</string> <string name="_s_removed_from_history">«%s» удалено из истории</string>
<string name="_s_deleted_from_local_storage">«%s» удалено с устройства</string> <string name="_s_deleted_from_local_storage">«%s» удалено с устройства</string>
<string name="wait_for_loading_finish">Дождитесь завершения загрузки…</string> <string name="wait_for_loading_finish">Дождитесь завершения загрузки…</string>
<string name="save_page">Сохранить страницу</string> <string name="save_page">Сохранить страницу</string>
<string name="page_saved">Сохранено</string> <string name="page_saved">Сохранено</string>
<string name="share_image">Поделиться изображением</string> <string name="share_image">Поделиться изображением</string>
<string name="_import">Импорт</string> <string name="_import">Импорт</string>
<string name="delete">Удалить</string> <string name="delete">Удалить</string>
<string name="operation_not_supported">Операция не поддерживается</string> <string name="operation_not_supported">Операция не поддерживается</string>
<string name="text_file_not_supported">Выберите CBZ-файл или ZIP</string> <string name="text_file_not_supported">Выберите CBZ-файл или ZIP</string>
<string name="no_description">Нет описания</string> <string name="no_description">Нет описания</string>
<string name="history_and_cache">История и кэш</string> <string name="history_and_cache">История и кэш</string>
<string name="clear_pages_cache">Очистить кэш страниц</string> <string name="clear_pages_cache">Очистить кэш страниц</string>
<string name="cache">Кэш</string> <string name="cache">Кэш</string>
<string name="text_file_sizes">Б|кБ|МБ|ГБ|ТБ</string> <string name="text_file_sizes">Б|кБ|МБ|ГБ|ТБ</string>
<string name="standard">Стандартный</string> <string name="standard">Стандартный</string>
<string name="webtoon">Манхва</string> <string name="webtoon">Манхва</string>
<string name="read_mode">Режим чтения</string> <string name="read_mode">Режим чтения</string>
<string name="grid_size">Размер таблицы</string> <string name="grid_size">Размер таблицы</string>
<string name="search_on_s">Поиск по %s</string> <string name="search_on_s">Поиск по %s</string>
<string name="delete_manga">Удалить мангу</string> <string name="delete_manga">Удалить мангу</string>
<string name="text_delete_local_manga">Удалить \"%s\" с устройства навсегда\?</string> <string name="text_delete_local_manga">Удалить \"%s\" с устройства навсегда\?</string>
<string name="reader_settings">Настройки чтения</string> <string name="reader_settings">Настройки чтения</string>
<string name="switch_pages">Листание страниц</string> <string name="switch_pages">Листание страниц</string>
<string name="taps_on_edges">Нажатия по краям</string> <string name="taps_on_edges">Нажатия по краям</string>
<string name="volume_buttons">Кнопки громкости</string> <string name="volume_buttons">Кнопки громкости</string>
<string name="_continue">Продолжить</string> <string name="_continue">Продолжить</string>
<string name="warning">Предупреждение</string> <string name="warning">Предупреждение</string>
<string name="network_consumption_warning">Это может привести к расходу большого количества трафика</string> <string name="network_consumption_warning">Это может привести к расходу большого количества трафика</string>
<string name="dont_ask_again">Больше не спрашивать</string> <string name="dont_ask_again">Больше не спрашивать</string>
<string name="cancelling_">Отмена…</string> <string name="cancelling_">Отмена…</string>
<string name="error">Ошибка</string> <string name="error">Ошибка</string>
<string name="clear_thumbs_cache">Очистить кэш миниатюр</string> <string name="clear_thumbs_cache">Очистить кэш миниатюр</string>
<string name="clear_search_history">Очистить историю поиска</string> <string name="clear_search_history">Очистить историю поиска</string>
<string name="search_history_cleared">Очищено</string> <string name="search_history_cleared">Очищено</string>
<string name="gestures_only">Только жесты</string> <string name="gestures_only">Только жесты</string>
<string name="internal_storage">Внутренний накопитель</string> <string name="internal_storage">Внутренний накопитель</string>
<string name="external_storage">Внешнее хранилище</string> <string name="external_storage">Внешнее хранилище</string>
<string name="domain">Домен</string> <string name="domain">Домен</string>
<string name="application_update">Проверять наличие новых версий приложения</string> <string name="application_update">Проверять наличие новых версий приложения</string>
<string name="app_update_available">Доступна новая версия приложения</string> <string name="app_update_available">Доступна новая версия приложения</string>
<string name="show_notification_app_update">Показывать уведомление, если доступна новая версия</string> <string name="show_notification_app_update">Показывать уведомление, если доступна новая версия</string>
<string name="open_in_browser">Открыть в веб-браузере</string> <string name="open_in_browser">Открыть в веб-браузере</string>
<string name="large_manga_save_confirm">В этой манге %s. Сохранить их все\?</string> <string name="large_manga_save_confirm">В этой манге %s. Сохранить их все\?</string>
<string name="save_manga">Сохранить</string> <string name="save_manga">Сохранить</string>
<string name="notifications">Уведомления</string> <string name="notifications">Уведомления</string>
<string name="enabled_d_of_d">Включено %1$d из %2$d</string> <string name="enabled_d_of_d">Включено %1$d из %2$d</string>
<string name="new_chapters">Новые главы</string> <string name="new_chapters">Новые главы</string>
<string name="download">Загрузить</string> <string name="download">Загрузить</string>
<string name="read_from_start">Читать с начала</string> <string name="read_from_start">Читать с начала</string>
<string name="restart">Перезапустить</string> <string name="restart">Перезапустить</string>
<string name="notifications_settings">Настройки уведомлений</string> <string name="notifications_settings">Настройки уведомлений</string>
<string name="notification_sound">Звук уведомления</string> <string name="notification_sound">Звук уведомления</string>
<string name="light_indicator">Светодиодная индикация</string> <string name="light_indicator">Светодиодная индикация</string>
<string name="vibration">Вибросигнал</string> <string name="vibration">Вибросигнал</string>
<string name="favourites_categories">Категории избранного</string> <string name="favourites_categories">Категории избранного</string>
<string name="categories_">Категории…</string> <string name="categories_">Категории…</string>
<string name="rename">Переименовать</string> <string name="rename">Переименовать</string>
<string name="category_delete_confirm">Удалить категорию \"%s\" из избранного\? <string name="category_delete_confirm">Удалить категорию \"%s\" из избранного\?
\nВся манга в ней будет потеряна.</string> \nВся манга в ней будет потеряна.</string>
<string name="remove_category">Удалить</string> <string name="remove_category">Удалить</string>
<string name="text_empty_holder_primary">Как-то здесь пусто…</string> <string name="text_empty_holder_primary">Как-то здесь пусто…</string>
<string name="text_search_holder_secondary">Попробуйте переформулировать запрос.</string> <string name="text_search_holder_secondary">Попробуйте переформулировать запрос.</string>
<string name="text_categories_holder">Вы можете использовать категории для организации своих избранных. Нажмите «+», чтобы создать категорию</string> <string name="text_categories_holder">Вы можете использовать категории для организации своих избранных. Нажмите «+», чтобы создать категорию</string>
<string name="text_history_holder_primary">То, что вы прочитаете, будет отображено здесь</string> <string name="text_history_holder_primary">То, что вы прочитаете, будет отображено здесь</string>
<string name="text_history_holder_secondary">Найдите, что почитать, в боковом меню.</string> <string name="text_history_holder_secondary">Найдите, что почитать, в боковом меню.</string>
<string name="text_local_holder_primary">Сохраните что-нибудь</string> <string name="text_local_holder_primary">Сохраните что-нибудь</string>
<string name="text_local_holder_secondary">Сохраните что-нибудь из онлайн-каталога или импортируйте из файла.</string> <string name="text_local_holder_secondary">Сохраните что-нибудь из онлайн-каталога или импортируйте из файла.</string>
<string name="manga_shelf">Полка</string> <string name="manga_shelf">Полка</string>
<string name="recent_manga">Недавнее</string> <string name="recent_manga">Недавнее</string>
<string name="pages_animation">Анимация листания</string> <string name="pages_animation">Анимация листания</string>
<string name="manga_save_location">Папка для загрузок</string> <string name="manga_save_location">Папка для загрузок</string>
<string name="not_available">Недоступно</string> <string name="not_available">Недоступно</string>
<string name="cannot_find_available_storage">Нет доступного хранилища</string> <string name="cannot_find_available_storage">Нет доступного хранилища</string>
<string name="other_storage">Другое хранилище</string> <string name="other_storage">Другое хранилище</string>
<string name="done">Готово</string> <string name="done">Готово</string>
<string name="all_favourites">Всё избранное</string> <string name="all_favourites">Всё избранное</string>
<string name="favourites_category_empty">Категория пуста</string> <string name="favourites_category_empty">Категория пуста</string>
<string name="read_later">Прочитать позже</string> <string name="read_later">Прочитать позже</string>
<string name="updates">Обновления</string> <string name="updates">Обновления</string>
<string name="text_feed_holder">Новые главы из того, что вы читаете, будут показаны здесь</string> <string name="text_feed_holder">Новые главы из того, что вы читаете, будут показаны здесь</string>
<string name="search_results">Результаты поиска</string> <string name="search_results">Результаты поиска</string>
<string name="related">Похожие</string> <string name="related">Похожие</string>
<string name="new_version_s">Новая версия: %s</string> <string name="new_version_s">Новая версия: %s</string>
<string name="size_s">Размер: %s</string> <string name="size_s">Размер: %s</string>
<string name="waiting_for_network">Ожидание подключения…</string> <string name="waiting_for_network">Ожидание подключения…</string>
<string name="clear_updates_feed">Очистить ленту обновлений</string> <string name="clear_updates_feed">Очистить ленту обновлений</string>
<string name="updates_feed_cleared">Очищено</string> <string name="updates_feed_cleared">Очищено</string>
<string name="rotate_screen">Повернуть экран</string> <string name="rotate_screen">Повернуть экран</string>
<string name="update">Обновить</string> <string name="update">Обновить</string>
<string name="feed_will_update_soon">Обновление скоро начнётся</string> <string name="feed_will_update_soon">Обновление скоро начнётся</string>
<string name="track_sources">Следить за обновлениями</string> <string name="track_sources">Следить за обновлениями</string>
<string name="dont_check">Не проверять</string> <string name="dont_check">Не проверять</string>
<string name="enter_password">Введите пароль</string> <string name="enter_password">Введите пароль</string>
<string name="wrong_password">Неверный пароль</string> <string name="wrong_password">Неверный пароль</string>
<string name="protect_application">Защитить приложение</string> <string name="protect_application">Защитить приложение</string>
<string name="protect_application_summary">Запрашивать пароль при запуске Kotatsu</string> <string name="protect_application_summary">Запрашивать пароль при запуске Kotatsu</string>
<string name="repeat_password">Повторите пароль</string> <string name="repeat_password">Повторите пароль</string>
<string name="passwords_mismatch">Пароли не совпадают</string> <string name="passwords_mismatch">Пароли не совпадают</string>
<string name="about">О программе</string> <string name="about">О программе</string>
<string name="app_version">Версия %s</string> <string name="app_version">Версия %s</string>
<string name="check_for_updates">Проверить обновления</string> <string name="check_for_updates">Проверить обновления</string>
<string name="checking_for_updates">Проверка обновления…</string> <string name="checking_for_updates">Проверка обновления…</string>
<string name="update_check_failed">Не удалось проверить обновления</string> <string name="update_check_failed">Не удалось проверить обновления</string>
<string name="no_update_available">Нет доступных обновлений</string> <string name="no_update_available">Нет доступных обновлений</string>
<string name="right_to_left">Справа налево (←)</string> <string name="right_to_left">Справа налево (←)</string>
<string name="create_category">Создать категорию</string> <string name="create_category">Создать категорию</string>
<string name="scale_mode">Масштабирование</string> <string name="scale_mode">Масштабирование</string>
<string name="zoom_mode_fit_center">Вписать в экран</string> <string name="zoom_mode_fit_center">Вписать в экран</string>
<string name="zoom_mode_fit_height">Подогнать по высоте</string> <string name="zoom_mode_fit_height">Подогнать по высоте</string>
<string name="zoom_mode_fit_width">Подогнать по ширине</string> <string name="zoom_mode_fit_width">Подогнать по ширине</string>
<string name="zoom_mode_keep_start">Исходный размер</string> <string name="zoom_mode_keep_start">Исходный размер</string>
<string name="black_dark_theme">Чёрная</string> <string name="black_dark_theme">Чёрная</string>
<string name="black_dark_theme_summary">Потребляет меньше энергии на экранах AMOLED</string> <string name="black_dark_theme_summary">Потребляет меньше энергии на экранах AMOLED</string>
<string name="backup_restore">Резервное копирование и восстановление</string> <string name="backup_restore">Резервное копирование и восстановление</string>
<string name="create_backup">Создать резервную копию</string> <string name="create_backup">Создать резервную копию</string>
<string name="restore_backup">Восстановить данные</string> <string name="restore_backup">Восстановить данные</string>
<string name="data_restored">Восстановлено</string> <string name="data_restored">Восстановлено</string>
<string name="preparing_">Подготовка…</string> <string name="preparing_">Подготовка…</string>
<string name="file_not_found">Файл не найден</string> <string name="file_not_found">Файл не найден</string>
<string name="data_restored_success">Все данные были восстановлены</string> <string name="data_restored_success">Все данные были восстановлены</string>
<string name="data_restored_with_errors">Данные были восстановлены, но возникли некоторые ошибки</string> <string name="data_restored_with_errors">Данные были восстановлены, но возникли некоторые ошибки</string>
<string name="backup_information">Вы можете создать резервную копию избранного и истории и потом восстановить их</string> <string name="backup_information">Вы можете создать резервную копию избранного и истории и потом восстановить их</string>
<string name="just_now">Только что</string> <string name="just_now">Только что</string>
<string name="yesterday">Вчера</string> <string name="yesterday">Вчера</string>
<string name="long_ago">Давно</string> <string name="long_ago">Давно</string>
<string name="group">Группировать</string> <string name="group">Группировать</string>
<string name="today">Сегодня</string> <string name="today">Сегодня</string>
<string name="tap_to_try_again">Попробовать ещё раз</string> <string name="tap_to_try_again">Попробовать ещё раз</string>
<string name="reader_mode_hint">Выбранный режим будет сохранён для текущей манги</string> <string name="reader_mode_hint">Выбранный режим будет сохранён для текущей манги</string>
<string name="silent">Без звука</string> <string name="silent">Без звука</string>
<string name="captcha_required">Необходимо пройти CAPTCHA</string> <string name="captcha_required">Необходимо пройти CAPTCHA</string>
<string name="captcha_solve">Пройти</string> <string name="captcha_solve">Пройти</string>
<string name="clear_cookies">Очистить куки</string> <string name="clear_cookies">Очистить куки</string>
<string name="cookies_cleared">Все файлы cookie были удалены</string> <string name="cookies_cleared">Все файлы cookie были удалены</string>
<string name="chapters_checking_progress">Проверка новых глав: %1$d из %2$d</string> <string name="chapters_checking_progress">Проверка новых глав: %1$d из %2$d</string>
<string name="clear_feed">Очистить ленту</string> <string name="clear_feed">Очистить ленту</string>
<string name="text_clear_updates_feed_prompt">Удалить всю историю обновлений навсегда\?</string> <string name="text_clear_updates_feed_prompt">Удалить всю историю обновлений навсегда\?</string>
<string name="check_for_new_chapters">Проверка новых глав</string> <string name="check_for_new_chapters">Проверка новых глав</string>
<string name="reverse">В обратном порядке</string> <string name="reverse">В обратном порядке</string>
<string name="sign_in">Войти</string> <string name="sign_in">Войти</string>
<string name="auth_required">Авторизуйтесь, чтобы просмотреть этот контент</string> <string name="auth_required">Авторизуйтесь, чтобы просмотреть этот контент</string>
<string name="default_s">По умолчанию: %s</string> <string name="default_s">По умолчанию: %s</string>
<string name="_and_x_more">…и ещё %1$d</string> <string name="_and_x_more">…и ещё %1$d</string>
<string name="next">Далее</string> <string name="next">Далее</string>
<string name="protect_application_subtitle">Введите пароль для запуска приложения</string> <string name="protect_application_subtitle">Введите пароль для запуска приложения</string>
<string name="confirm">Подтвердить</string> <string name="confirm">Подтвердить</string>
<string name="password_length_hint">Пароль должен состоять из 4 символов или более</string> <string name="password_length_hint">Пароль должен состоять из 4 символов или более</string>
<string name="search_only_on_s">Поиск только по %s</string> <string name="search_only_on_s">Поиск только по %s</string>
<string name="other">Другие</string> <string name="other">Другие</string>
<string name="welcome">Добро пожаловать</string> <string name="welcome">Добро пожаловать</string>
<string name="text_clear_search_history_prompt">Удалить все последние поисковые запросы навсегда\?</string> <string name="text_clear_search_history_prompt">Удалить все последние поисковые запросы навсегда\?</string>
<string name="backup_saved">Резервная копия сохранена</string> <string name="backup_saved">Резервная копия сохранена</string>
<string name="tracker_warning">Некоторые устройства имеют различное поведение системы, что может привести к нарушению фоновых задач.</string> <string name="tracker_warning">Некоторые устройства имеют различное поведение системы, что может привести к нарушению фоновых задач.</string>
<string name="read_more">Подробнее</string> <string name="read_more">Подробнее</string>
<string name="queued">В очереди</string> <string name="queued">В очереди</string>
<string name="text_downloads_holder">Нет активных загрузок</string> <string name="text_downloads_holder">Нет активных загрузок</string>
<string name="chapter_is_missing">Глава отсутствует</string> <string name="chapter_is_missing">Глава отсутствует</string>
<string name="chapter_is_missing_text">Скачайте или прочитайте эту недостающую главу онлайн.</string> <string name="chapter_is_missing_text">Скачайте или прочитайте эту недостающую главу онлайн.</string>
<string name="about_app_translation_summary">Помочь с переводом приложения</string> <string name="about_app_translation_summary">Помочь с переводом приложения</string>
<string name="about_app_translation">Перевод</string> <string name="about_app_translation">Перевод</string>
<string name="about_feedback_4pda">Тема на 4PDA</string> <string name="about_feedback_4pda">Тема на 4PDA</string>
<string name="about_feedback">Обратная связь</string> <string name="about_feedback">Обратная связь</string>
<string name="auth_complete">Авторизация выполнена</string> <string name="auth_complete">Авторизация выполнена</string>
<string name="auth_not_supported_by">Вход в %s не поддерживается</string> <string name="auth_not_supported_by">Вход в %s не поддерживается</string>
<string name="text_clear_cookies_prompt">Вы выйдете из всех источников</string> <string name="text_clear_cookies_prompt">Вы выйдете из всех источников</string>
<string name="genres">Жанры</string> <string name="genres">Жанры</string>
<string name="state_finished">Завершено</string> <string name="state_finished">Завершено</string>
<string name="state_ongoing">Онгоинг</string> <string name="state_ongoing">Онгоинг</string>
<string name="date_format">Формат даты</string> <string name="date_format">Формат даты</string>
<string name="system_default">По умолчанию</string> <string name="system_default">По умолчанию</string>
<string name="exclude_nsfw_from_history">Исключить NSFW мангу из истории</string> <string name="exclude_nsfw_from_history">Исключить NSFW мангу из истории</string>
<string name="error_empty_name">Вы должны ввести имя</string> <string name="error_empty_name">Вы должны ввести имя</string>
<string name="show_pages_numbers">Показывать номера страницы</string> <string name="show_pages_numbers">Показывать номера страницы</string>
<string name="enabled_sources">Включенные источники</string> <string name="enabled_sources">Включенные источники</string>
<string name="available_sources">Доступные источники</string> <string name="available_sources">Доступные источники</string>
<string name="dynamic_theme">Динамическая тема</string> <string name="dynamic_theme">Динамическая тема</string>
<string name="dynamic_theme_summary">Применяет тему приложения, основанную на цветовой палитре обоев на устройстве</string> <string name="dynamic_theme_summary">Применяет тему приложения, основанную на цветовой палитре обоев на устройстве</string>
<string name="screenshots_policy">Политика скриншотов</string> <string name="screenshots_policy">Политика скриншотов</string>
<string name="screenshots_allow">Разрешить</string> <string name="screenshots_allow">Разрешить</string>
<string name="screenshots_block_nsfw">Запретить для NSFW</string> <string name="screenshots_block_nsfw">Запретить для NSFW</string>
<string name="screenshots_block_all">Всегда блокировать</string> <string name="screenshots_block_all">Всегда блокировать</string>
<string name="suggestions">Рекомендации</string> <string name="suggestions">Рекомендации</string>
<string name="suggestions_enable">Включить рекомендации</string> <string name="suggestions_enable">Включить рекомендации</string>
<string name="suggestions_summary">Предлагать мангу на основе Ваших предпочтений</string> <string name="suggestions_summary">Предлагать мангу на основе Ваших предпочтений</string>
<string name="suggestions_info">Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы</string> <string name="suggestions_info">Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы</string>
<string name="text_suggestion_holder">Начните читать мангу, чтобы получать персональные предложения</string> <string name="text_suggestion_holder">Начните читать мангу, чтобы получать персональные предложения</string>
<string name="exclude_nsfw_from_suggestions">Не предлагать NSFW мангу</string> <string name="exclude_nsfw_from_suggestions">Не предлагать NSFW мангу</string>
<string name="enabled">Включено</string> <string name="enabled">Включено</string>
<string name="disabled">Выключено</string> <string name="disabled">Выключено</string>
<string name="filter_load_error">Не удалось загрузить список жанров</string> <string name="filter_load_error">Не удалось загрузить список жанров</string>
<string name="computing_">Вычисление…</string> <string name="computing_">Вычисление…</string>
<string name="report_github">Создать проблему на GitHub</string> <string name="report_github">Создать проблему на GitHub</string>
<string name="importing_progress">Импорт манги: %1$d из %2$d</string> <string name="importing_progress">Импорт манги: %1$d из %2$d</string>
<string name="reset_filter">Сбросить фильтр</string> <string name="reset_filter">Сбросить фильтр</string>
<string name="find_genre">Поиск по жанрам</string> <string name="find_genre">Поиск по жанрам</string>
<string name="onboard_text">Выберите языки, на которых Вы хоите читать мангу. Это можно будет изменить позже в настройках.</string> <string name="onboard_text">Выберите языки, на которых Вы хоите читать мангу. Это можно будет изменить позже в настройках.</string>
<string name="never">Никогда</string> <string name="never">Никогда</string>
<string name="only_using_wifi">Только по Wi-Fi</string> <string name="only_using_wifi">Только по Wi-Fi</string>
<string name="always">Всегда</string> <string name="always">Всегда</string>
<string name="preload_pages">Предварительная загрузка страниц</string> <string name="preload_pages">Предварительная загрузка страниц</string>
<string name="logged_in_as">Вы авторизованы как %s</string> <string name="logged_in_as">Вы авторизованы как %s</string>
<string name="nsfw">18+</string> <string name="nsfw">18+</string>
<string name="various_languages">Разные языки</string> <string name="various_languages">Разные языки</string>
<string name="search_chapters">Найти главу</string> <string name="search_chapters">Найти главу</string>
<string name="chapters_empty">В этой манге нет глав</string> <string name="chapters_empty">В этой манге нет глав</string>
<string name="appearance">Оформление</string> <string name="appearance">Оформление</string>
<string name="content">Контент</string> <string name="content">Контент</string>
<string name="suggestions_updating">Обновление рекомендаций</string> <string name="suggestions_updating">Обновление рекомендаций</string>
<string name="suggestions_excluded_genres">Исключить жанры</string> <string name="suggestions_excluded_genres">Исключить жанры</string>
<string name="suggestions_excluded_genres_summary">Укажите жанры, которые Вы не хотите видеть в рекомендациях</string> <string name="suggestions_excluded_genres_summary">Укажите жанры, которые Вы не хотите видеть в рекомендациях</string>
<string name="text_delete_local_manga_batch">Удалить выбранную мангу с накопителя?</string> <string name="text_delete_local_manga_batch">Удалить выбранную мангу с накопителя?</string>
<string name="removal_completed">Удаление завершено</string> <string name="removal_completed">Удаление завершено</string>
<string name="batch_manga_save_confirm">Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе</string> <string name="batch_manga_save_confirm">Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе</string>
<string name="parallel_downloads">Загружать параллельно</string> <string name="parallel_downloads">Загружать параллельно</string>
<string name="download_slowdown">Замедление загрузки</string> <string name="download_slowdown">Замедление загрузки</string>
<string name="download_slowdown_summary">Помогает избежать блокировки IP-адреса</string> <string name="download_slowdown_summary">Помогает избежать блокировки IP-адреса</string>
<string name="local_manga_processing">Обработка сохранённой манги</string> <string name="local_manga_processing">Обработка сохранённой манги</string>
<string name="chapters_will_removed_background">Главы будут удалены в фоновом режиме. Это может занять какое-то время</string> <string name="chapters_will_removed_background">Главы будут удалены в фоновом режиме. Это может занять какое-то время</string>
<string name="hide">Скрыть</string> <string name="hide">Скрыть</string>
<string name="new_sources_text">Доступны новые источники манги</string> <string name="new_sources_text">Доступны новые источники манги</string>
<string name="check_new_chapters_title">Проверять новые главы и уведомлять о них</string> <string name="check_new_chapters_title">Проверять новые главы и уведомлять о них</string>
<string name="show_notification_new_chapters_on">Вы будете получать уведомления об обновлении манги, которую Вы читаете</string> <string name="show_notification_new_chapters_on">Вы будете получать уведомления об обновлении манги, которую Вы читаете</string>
<string name="show_notification_new_chapters_off">Вы не будете получать уведомления, но новые главы будут отображаться в списке</string> <string name="show_notification_new_chapters_off">Вы не будете получать уведомления, но новые главы будут отображаться в списке</string>
<string name="notifications_enable">Включить уведомления</string> <string name="notifications_enable">Включить уведомления</string>
<string name="name">Название</string> <string name="name">Название</string>
<string name="edit">Изменить</string> <string name="edit">Изменить</string>
<string name="edit_category">Изменить категорию</string> <string name="edit_category">Изменить категорию</string>
<string name="tracking">Отслеживание</string> <string name="tracking">Отслеживание</string>
<string name="empty_favourite_categories">Нет категорий избранного</string> <string name="empty_favourite_categories">Нет категорий избранного</string>
<string name="bookmark_add">Добавить закладку</string> <string name="bookmark_add">Добавить закладку</string>
<string name="bookmark_remove">Удалить закладку</string> <string name="bookmark_remove">Удалить закладку</string>
<string name="bookmarks">Закладки</string> <string name="bookmarks">Закладки</string>
<string name="bookmark_removed">Закладка удалена</string> <string name="bookmark_removed">Закладка удалена</string>
<string name="bookmark_added">Закладка добавлена</string> <string name="bookmark_added">Закладка добавлена</string>
<string name="undo">Отменить</string> <string name="undo">Отменить</string>
<string name="removed_from_history">Удалено из истории</string> <string name="removed_from_history">Удалено из истории</string>
<string name="dns_over_https">DNS через HTTPS</string> <string name="dns_over_https">DNS через HTTPS</string>
<string name="default_mode">Режим по умолчанию</string> <string name="default_mode">Режим по умолчанию</string>
<string name="detect_reader_mode">Автоопределение режима чтения</string> <string name="detect_reader_mode">Автоопределение режима чтения</string>
<string name="detect_reader_mode_summary">Автоматически определяет, является ли манга веб-комиксом</string> <string name="detect_reader_mode_summary">Автоматически определяет, является ли манга веб-комиксом</string>
<string name="disable_battery_optimization">Отключить оптимизацию батареи</string> <string name="disable_battery_optimization">Отключить оптимизацию батареи</string>
<string name="disable_battery_optimization_summary">Помогает с фоновой проверкой обновлений</string> <string name="disable_battery_optimization_summary">Помогает с фоновой проверкой обновлений</string>
<string name="crash_text">Что-то пошло не так. Пожалуйста, отправьте отчёт разработчикам, чтобы помочь всё исправить</string> <string name="crash_text">Что-то пошло не так. Пожалуйста, отправьте отчёт разработчикам, чтобы помочь всё исправить</string>
<string name="send">Отправить</string> <string name="send">Отправить</string>
<string name="disable_all">Отключить все</string> <string name="disable_all">Отключить все</string>
<string name="use_fingerprint">Использовать отпечаток пальца, если доступно</string> <string name="use_fingerprint">Использовать отпечаток пальца, если доступно</string>
<string name="appwidget_shelf_description">Манга из Вашего избранного</string> <string name="appwidget_shelf_description">Манга из Вашего избранного</string>
<string name="appwidget_recent_description">Манга, которую Вы недавно читали</string> <string name="appwidget_recent_description">Манга, которую Вы недавно читали</string>
<string name="status_reading">Читаю</string>
<string name="status_planned">Запланировано</string>
<string name="status_on_hold">Отложено</string>
<string name="status_dropped">Заброшено</string>
<string name="status_completed">Завершено</string>
<string name="show_reading_indicators_summary">Показать процент прочитанного в истории и избранном</string>
<string name="exclude_nsfw_from_history_summary">Манга, помеченная как NSFW, никогда не будет добавлена в историю и ваш прогресс чтения не будет сохранен</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="report">Отчёт</string>
<string name="logout">Выйти</string>
<string name="status_re_reading">Перечитываю</string>
<string name="show_reading_indicators">Показать индикаторы прогресса чтения</string>
<string name="data_deletion">Удаление данных</string>
<string name="clear_cookies_summary">Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы</string>
<string name="show_all">Показать все</string>
</resources> </resources>

View File

@@ -31,7 +31,7 @@
<string name="clear_cookies">清除cookies</string> <string name="clear_cookies">清除cookies</string>
<string name="chapters_checking_progress">检查新的章节: %1$d/%2$d</string> <string name="chapters_checking_progress">检查新的章节: %1$d/%2$d</string>
<string name="error_empty_name">你必须输入一个名称</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_summary">根据你的喜好推荐漫画</string>
<string name="suggestions_info">所有的数据都在这个设备上进行本地分析. 您的个人数据不会被转移到任何服务机构</string> <string name="suggestions_info">所有的数据都在这个设备上进行本地分析. 您的个人数据不会被转移到任何服务机构</string>
<string name="never">从不</string> <string name="never">从不</string>
@@ -284,7 +284,7 @@
<string name="bookmark_remove">删除书签</string> <string name="bookmark_remove">删除书签</string>
<string name="bookmarks">书签</string> <string name="bookmarks">书签</string>
<string name="bookmark_removed">删除书签</string> <string name="bookmark_removed">删除书签</string>
<string name="bookmark_added">加入书签</string> <string name="bookmark_added">添加书签</string>
<string name="undo">撤销</string> <string name="undo">撤销</string>
<string name="removed_from_history">从历史中删除</string> <string name="removed_from_history">从历史中删除</string>
<string name="dns_over_https">DNS over HTTPS</string> <string name="dns_over_https">DNS over HTTPS</string>
@@ -295,4 +295,22 @@
<string name="disable_battery_optimization_summary">帮助进行背景更新检查</string> <string name="disable_battery_optimization_summary">帮助进行背景更新检查</string>
<string name="crash_text">出了点问题. 请向开发人员提交一份错误报告以帮助我们修复它.</string> <string name="crash_text">出了点问题. 请向开发人员提交一份错误报告以帮助我们修复它.</string>
<string name="send">发送</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> </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="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="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="show_all">Show all</string>
<string name="invalid_domain_message">Invalid domain</string>
</resources> </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)
}
}

View File

@@ -1,4 +1,6 @@
Kotatsu is a free and open source manga reader for Android.<br> Kotatsu is a free and open source manga reader for Android.
**Main Features:** **Main Features:**
- Online manga catalogues - Online manga catalogues
@@ -9,5 +11,5 @@ Kotatsu is a free and open source manga reader for Android.<br>
- Tablet-optimized material design UI - Tablet-optimized material design UI
- Standard and Webtoon-optimized reader - Standard and Webtoon-optimized reader
- Notifications about new chapters with updates feed - Notifications about new chapters with updates feed
- Available in multiple languages - Shikimori integration (manga tracking)
- Password protect access to the app - Password/fingerprint protect access to the app

View File

@@ -1,4 +1,6 @@
Kotatsu - приложения для чтения манги с открытым исходным кодом.<br> Kotatsu - приложения для чтения манги с открытым исходным кодом.
**Основные возможности:** **Основные возможности:**
- Онлайн каталоги с мангой - Онлайн каталоги с мангой
@@ -9,3 +11,5 @@ Kotatsu - приложения для чтения манги с открыты
- Интерфейс также оптимизирован для планшетов - Интерфейс также оптимизирован для планшетов
- Поддержка манхвы (Webtoon) - Поддержка манхвы (Webtoon)
- Уведомления о новых главах и лента обновлений - Уведомления о новых главах и лента обновлений
- Интеграция с Shikimori
- Защита доступа в приложение паролем/отпечатком пальца