Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d4c1b751e | |||
| 91b17ef4a2 | |||
| 9b748f7334 | |||
| 2deaed2067 | |||
| fb608ed30a | |||
| 8e43afe408 | |||
| 73df680214 | |||
| fa4aa154a3 | |||
| cf7cdbc41b | |||
| c2561a1de0 | |||
| a36abe0272 | |||
| 5b10d697f6 | |||
| e0f07ccc3b | |||
|
|
938ea8fb73 | ||
|
|
ea6a338128 | ||
| ce3a668103 | |||
| 557c2b018a | |||
| 3add01d57e | |||
| 2ad1ea98f1 | |||
| 3121532217 | |||
| 20ac12ca0d | |||
| f0b222140e | |||
| 2a35ca6094 | |||
| 93f9636916 | |||
| 2c24aba558 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
64
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
Normal 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
|
||||||
91
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
91
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -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
|
|
||||||
25
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
25
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -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
|
||||||
27
README.md
27
README.md
@@ -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
|
||||||
|
|
||||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](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.
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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]" />
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
Kotatsu - приложения для чтения манги с открытым исходным кодом.<br>
|
Kotatsu - приложения для чтения манги с открытым исходным кодом.
|
||||||
|
|
||||||
|
|
||||||
**Основные возможности:**
|
**Основные возможности:**
|
||||||
|
|
||||||
- Онлайн каталоги с мангой
|
- Онлайн каталоги с мангой
|
||||||
@@ -9,3 +11,5 @@ Kotatsu - приложения для чтения манги с открыты
|
|||||||
- Интерфейс также оптимизирован для планшетов
|
- Интерфейс также оптимизирован для планшетов
|
||||||
- Поддержка манхвы (Webtoon)
|
- Поддержка манхвы (Webtoon)
|
||||||
- Уведомления о новых главах и лента обновлений
|
- Уведомления о новых главах и лента обновлений
|
||||||
|
- Интеграция с Shikimori
|
||||||
|
- Защита доступа в приложение паролем/отпечатком пальца
|
||||||
|
|||||||
Reference in New Issue
Block a user